1
0
mirror of synced 2025-12-23 21:07:12 -05:00

Picker improvements (#21765)

* close Language and ArticleVersion pickers after click

* cleanup ArticleGridLayout due to VersionPicker changes

* fix tsc errors resulting from primer upgrade

* fix / update tests

* cleanup mobile pickers visual consistency

* use btn-sm on VersionPicker

* update translation and close on click for enterprise releases

Co-authored-by: Kevin Heis <heiskr@users.noreply.github.com>
This commit is contained in:
Mike Surowiec
2021-09-30 16:22:13 -04:00
committed by GitHub
parent b9d3257689
commit d81f51ebf7
22 changed files with 465 additions and 653 deletions

View File

@@ -37,7 +37,7 @@ export function Link(props: Props) {
}
return (
<NextLink href={href || ''} locale={locale || false}>
<NextLink href={locale ? `/${locale}${href}` : href || ''} locale={locale || false}>
{/* eslint-disable-next-line jsx-a11y/anchor-has-content */}
<a rel={isExternal ? 'noopener' : ''} {...restProps} />
</NextLink>

View File

@@ -171,7 +171,7 @@ export function Search({
)}
>
{results.length > 0 ? (
<ol data-testid="search-results" className="d-block mt-2">
<ol data-testid="search-results" className="d-block mt-4">
{results.map(({ url, breadcrumbs, heading, title, content }, index) => {
const isActive = index === activeHit
return (

View File

@@ -0,0 +1,126 @@
import { useRouter } from 'next/router'
import cx from 'classnames'
import { Dropdown, Heading, Details, Box, Text, useDetails } from '@primer/components'
import { ArrowRightIcon, ChevronDownIcon } from '@primer/octicons-react'
import { Link } from 'components/Link'
import { useMainContext } from 'components/context/MainContext'
import { useVersion } from 'components/hooks/useVersion'
import { useTranslation } from 'components/hooks/useTranslation'
type Props = {
hideLabel?: boolean
variant?: 'default' | 'compact' | 'inline'
popoverVariant?: 'inline' | 'dropdown'
}
export const VersionPicker = ({ variant = 'default', popoverVariant, hideLabel }: Props) => {
const router = useRouter()
const { currentVersion } = useVersion()
const { allVersions, page, enterpriseServerVersions } = useMainContext()
const { getDetailsProps, setOpen } = useDetails({ closeOnOutsideClick: true })
const { t } = useTranslation('pages')
if (page.permalinks && page.permalinks.length <= 1) {
return null
}
return (
<>
{!hideLabel && (
<Heading as="span" fontSize={1} className="d-none d-xl-inline-block mb-1">
{t('article_version')}
</Heading>
)}
<div>
<Details
{...getDetailsProps()}
className={cx(
'position-relative details-reset',
variant === 'inline' ? 'd-block' : 'd-inline-block'
)}
data-testid="article-version-picker"
>
{(variant === 'compact' || variant === 'inline') && (
<summary
className="d-block btn btn-invisible color-text-primary"
aria-haspopup="true"
aria-label="Toggle version list"
>
{variant === 'inline' ? (
<div className="d-flex flex-items-center flex-justify-between">
<Text>{allVersions[currentVersion].versionTitle}</Text>
<ChevronDownIcon size={24} className="arrow ml-md-1" />
</div>
) : (
<>
<Text>{allVersions[currentVersion].versionTitle}</Text>
<Dropdown.Caret />
</>
)}
</summary>
)}
{variant === 'default' && (
<summary aria-haspopup="true" className="btn btn-sm">
<Text>{allVersions[currentVersion].versionTitle}</Text>
<Dropdown.Caret />
</summary>
)}
{popoverVariant === 'inline' ? (
<Box py="2">
{(page.permalinks || []).map((permalink) => {
return (
<Dropdown.Item key={permalink.href} onClick={() => setOpen(false)}>
<Link href={permalink.href}>{permalink.pageVersionTitle}</Link>
</Dropdown.Item>
)
})}
<Box mt={1}>
<Link
onClick={() => {
setOpen(false)
}}
href={`/${router.locale}/${enterpriseServerVersions[0]}/admin/all-releases`}
className="f6 no-underline color-text-tertiary pl-3 pr-2 no-wrap"
>
{t('all_enterprise_releases')}{' '}
<ArrowRightIcon verticalAlign="middle" size={15} className="mr-2" />
</Link>
</Box>
</Box>
) : (
<Dropdown.Menu direction="sw" style={{ width: 'unset' }}>
{(page.permalinks || []).map((permalink) => {
return (
<Dropdown.Item key={permalink.href} onClick={() => setOpen(false)}>
<Link href={permalink.href}>{permalink.pageVersionTitle}</Link>
</Dropdown.Item>
)
})}
<Box
borderColor="border.default"
borderTopWidth={1}
borderTopStyle="solid"
mt={2}
pt={2}
pb={1}
>
<Link
onClick={() => {
setOpen(false)
}}
href={`/${router.locale}/${enterpriseServerVersions[0]}/admin/all-releases`}
className="f6 no-underline color-text-tertiary pl-3 pr-2 no-wrap"
>
{t('all_enterprise_releases')}{' '}
<ArrowRightIcon verticalAlign="middle" size={15} className="mr-2" />
</Link>
</Box>
</Dropdown.Menu>
)}
</Details>
</div>
</>
)
}

View File

@@ -1,44 +0,0 @@
@import "@primer/css/layout/index.scss";
@import "@primer/css/support/variables/layout.scss";
@import "@primer/css/marketing/support/variables.scss";
.container {
max-width: 720px;
@include breakpoint(xl) {
max-width: none;
display: grid;
grid-template-rows: auto 1fr;
grid-template-columns: minmax(500px, 720px) minmax(220px, 1fr);
grid-template-areas:
"top right-sidebar"
"bottom right-sidebar";
column-gap: $spacer-6;
}
@include breakpoint(xl) {
column-gap: $spacer-9;
}
}
.sidebar {
grid-area: right-sidebar;
}
.sidebarContent {
@include breakpoint(xl) {
position: sticky;
top: $spacer-4;
max-height: calc(100vh - #{$spacer-4});
overflow-y: auto;
padding-bottom: $spacer-4;
}
}
.head {
grid-area: top;
}
.content {
grid-area: bottom;
}

View File

@@ -1,30 +1,78 @@
import React from 'react'
import cx from 'classnames'
import styles from './ArticleGridLayout.module.scss'
import styled from 'styled-components'
import { Box, themeGet } from '@primer/components'
type Props = {
head?: React.ReactNode
intro?: React.ReactNode
topperSidebar?: React.ReactNode
topper?: React.ReactNode
toc?: React.ReactNode
children?: React.ReactNode
className?: string
}
export const ArticleGridLayout = ({ head, toc, children, className }: Props) => {
export const ArticleGridLayout = ({
intro,
topperSidebar,
topper,
toc,
children,
className,
}: Props) => {
return (
<div className={cx(styles.container, className)}>
{/* head */}
{head && <div className={styles.head}>{head}</div>}
{/* toc */}
<Container className={className}>
{topper && <Box gridArea="topper">{topper}</Box>}
{topperSidebar && <Box gridArea="topper-sidebar">{topperSidebar}</Box>}
{toc && (
<div className={cx(styles.sidebar, 'border-bottom border-xl-0 pb-4 mb-5 pb-xl-0 mb-xl-0')}>
<div className={styles.sidebarContent}>{toc}</div>
</div>
<SidebarContent
gridArea="sidebar"
alignSelf="flex-start"
className="border-bottom border-xl-0 pb-4 mb-5 pb-xl-0 mb-xl-0"
>
{toc}
</SidebarContent>
)}
{/* content */}
<div data-search="article-body" className={styles.content}>
{intro && <Box gridArea="intro">{intro}</Box>}
<Box gridArea="content" data-search="article-body">
{children}
</div>
</div>
</Box>
</Container>
)
}
const Container = styled(Box)`
max-width: 720px;
display: grid;
grid-template-areas:
'topper'
'topper-sidebar'
'intro'
'sidebar'
'content';
row-gap: ${themeGet('space.2')};
@media (min-width: ${themeGet('breakpoints.3')}) {
max-width: none;
grid-template-rows: auto 1fr;
grid-template-columns: minmax(500px, 720px) minmax(220px, 1fr);
grid-template-areas:
'topper topper-sidebar'
'intro sidebar'
'content sidebar';
column-gap: ${themeGet('space.9')};
row-gap: 0;
}
`
const SidebarContent = styled(Box)`
@media (min-width: ${themeGet('breakpoints.3')}) {
position: sticky;
padding-top: ${themeGet('space.4')};
top: 0;
max-height: calc(100vh - ${themeGet('space.4')});
overflow-y: auto;
padding-bottom: ${themeGet('space.4')};
}
`

View File

@@ -1,12 +1,12 @@
import { useRouter } from 'next/router'
import cx from 'classnames'
import { Heading } from '@primer/components'
import { ZapIcon, InfoIcon } from '@primer/octicons-react'
import { Callout } from 'components/ui/Callout'
import { Link } from 'components/Link'
import { DefaultLayout } from 'components/DefaultLayout'
import { ArticleTopper } from 'components/article/ArticleTopper'
import { ArticleTitle } from 'components/article/ArticleTitle'
import { useArticleContext } from 'components/context/ArticleContext'
import { useTranslation } from 'components/hooks/useTranslation'
@@ -14,6 +14,8 @@ import { LearningTrackNav } from './LearningTrackNav'
import { MarkdownContent } from 'components/ui/MarkdownContent'
import { Lead } from 'components/ui/Lead'
import { ArticleGridLayout } from './ArticleGridLayout'
import { VersionPicker } from 'components/VersionPicker'
import { Breadcrumbs } from 'components/Breadcrumbs'
// Mapping of a "normal" article to it's interactive counterpart
const interactiveAlternatives: Record<string, { href: string }> = {
@@ -44,12 +46,11 @@ export const ArticlePage = () => {
return (
<DefaultLayout>
<div className="container-xl px-3 px-md-6 my-4 my-lg-4">
<ArticleTopper />
<div className="container-xl px-3 px-md-6 my-4">
<ArticleGridLayout
className="mt-7"
head={
topper={<Breadcrumbs />}
topperSidebar={<VersionPicker />}
intro={
<>
<ArticleTitle>{title}</ArticleTitle>
@@ -124,11 +125,11 @@ export const ArticlePage = () => {
)}
{miniTocItems.length > 1 && (
<>
<h2 id="in-this-article" className="f5 mb-2">
<Heading as="h2" fontSize={1} id="in-this-article" className="mb-1">
<a className="Link--primary" href="#in-this-article">
{t('miniToc')}
</a>
</h2>
</Heading>
<ul className="list-style-none pl-0 f5 mb-0">
{miniTocItems.map((item) => {
return (

View File

@@ -4,7 +4,7 @@ type Props = {
export const ArticleTitle = ({ children }: Props) => {
return (
<div className="d-flex flex-items-baseline flex-justify-between">
<h1 className="my-4 border-bottom-0">{children}</h1>
<h1 className="mt-4 border-bottom-0">{children}</h1>
</div>
)
}

View File

@@ -1,18 +0,0 @@
import { Breadcrumbs } from 'components/Breadcrumbs'
import { ArticleVersionPicker } from 'components/article/ArticleVersionPicker'
export const ArticleTopper = () => {
return (
<div className="d-lg-flex flex-justify-between">
<div className="d-block d-lg-none mb-2">
<ArticleVersionPicker />
</div>
<div className="d-flex flex-items-center">
<Breadcrumbs />
</div>
<div className="d-none d-lg-block">
<ArticleVersionPicker />
</div>
</div>
)
}

View File

@@ -1,52 +0,0 @@
import { useRouter } from 'next/router'
import { Dropdown } from '@primer/components'
import { Link } from 'components/Link'
import { useMainContext } from 'components/context/MainContext'
import { useVersion } from 'components/hooks/useVersion'
import { useTranslation } from 'components/hooks/useTranslation'
export const ArticleVersionPicker = () => {
const router = useRouter()
const { currentVersion } = useVersion()
const { allVersions, page, enterpriseServerVersions } = useMainContext()
const { t } = useTranslation('pages')
if (page.permalinks && page.permalinks.length <= 1) {
return null
}
return (
<Dropdown
css={`
ul {
width: unset;
}
`}
data-testid="article-version-picker"
>
<summary className="btn btn-outline p-2 outline-none">
<span className="d-md-none d-xl-inline-block">{t('article_version')}</span>{' '}
{allVersions[currentVersion].versionTitle}
<Dropdown.Caret />
</summary>
<Dropdown.Menu direction="sw">
{(page.permalinks || []).map((permalink) => {
return (
<Dropdown.Item key={permalink.href}>
<Link href={permalink.href}>{permalink.pageVersionTitle}</Link>
</Dropdown.Item>
)
})}
<div className="pb-1">
<Link
href={`/${router.locale}/${enterpriseServerVersions[0]}/admin/all-releases`}
className="f6 no-underline color-text-tertiary pl-3 pr-2 no-wrap"
>
See all Enterprise releases
</Link>
</div>
</Dropdown.Menu>
</Dropdown>
)
}

View File

@@ -1,93 +0,0 @@
import cx from 'classnames'
import { useRouter } from 'next/router'
import { Dropdown, Details, useDetails } from '@primer/components'
import { ChevronDownIcon } from '@primer/octicons-react'
import { Link } from 'components/Link'
import { useMainContext } from 'components/context/MainContext'
import { useVersion } from 'components/hooks/useVersion'
type Props = {
variant?: 'inline'
}
export const HomepageVersionPicker = ({ variant }: Props) => {
const router = useRouter()
const { currentVersion } = useVersion()
const { getDetailsProps } = useDetails({})
const { allVersions, page, enterpriseServerVersions } = useMainContext()
if (page.permalinks && page.permalinks.length <= 1) {
return null
}
const label = allVersions[currentVersion].versionTitle
if (variant === 'inline') {
return (
<Details {...getDetailsProps()} className="details-reset">
<summary className="outline-none" aria-label="Toggle language list">
<div className="d-flex flex-items-center flex-justify-between py-2">
<span>{label}</span>
<ChevronDownIcon size={24} className="arrow ml-md-1" />
</div>
</summary>
<div>
{(page.permalinks || []).map((permalink) => {
return (
<Link
key={permalink.href}
href={permalink.href}
className={cx(
'd-block py-2',
permalink.href === router.asPath
? 'color-text-link text-underline active'
: 'Link--primary no-underline'
)}
>
{permalink.pageVersionTitle}
</Link>
)
})}
<Link
href={`/${router.locale}/${enterpriseServerVersions[0]}/admin/all-releases`}
className="f6 no-underline color-text-tertiary no-wrap"
>
See all Enterprise releases
</Link>
</div>
</Details>
)
}
return (
<Dropdown
css={`
ul {
width: unset;
}
`}
>
<summary>
{label}
<Dropdown.Caret />
</summary>
<Dropdown.Menu direction="sw">
{(page.permalinks || []).map((permalink) => {
return (
<Dropdown.Item key={permalink.href}>
<Link href={permalink.href}>{permalink.pageVersionTitle}</Link>
</Dropdown.Item>
)
})}
<div className="pb-1">
<Link
href={`/${router.locale}/${enterpriseServerVersions[0]}/admin/all-releases`}
className="f6 no-underline color-text-tertiary pl-3 pr-2 no-wrap"
>
See all Enterprise releases
</Link>
</div>
</Dropdown.Menu>
</Dropdown>
)
}

View File

@@ -1,7 +1,8 @@
import { DefaultLayout } from 'components/DefaultLayout'
import { TableOfContents } from 'components/landing/TableOfContents'
import { useTocLandingContext } from 'components/context/TocLandingContext'
import { ArticleTopper } from 'components/article/ArticleTopper'
import { VersionPicker } from 'components/VersionPicker'
import { Breadcrumbs } from 'components/Breadcrumbs'
import { ArticleTitle } from 'components/article/ArticleTitle'
import { MarkdownContent } from 'components/ui/MarkdownContent'
import { ArticleList } from 'components/landing/ArticleList'
@@ -9,7 +10,7 @@ import { useTranslation } from 'components/hooks/useTranslation'
import { ArticleGridLayout } from 'components/article/ArticleGridLayout'
import { Callout } from 'components/ui/Callout'
import { Lead } from 'components/ui/Lead'
import { LearningTrackNav } from '../article/LearningTrackNav'
import { LearningTrackNav } from 'components/article/LearningTrackNav'
export const TocLanding = () => {
const {
@@ -26,10 +27,8 @@ export const TocLanding = () => {
return (
<DefaultLayout>
<div className="container-xl px-3 px-md-6 my-4 my-lg-4">
<ArticleTopper />
<ArticleGridLayout className="mt-7">
<div className="container-xl px-3 px-md-6 my-4">
<ArticleGridLayout topper={<Breadcrumbs />} topperSidebar={<VersionPicker />}>
<ArticleTitle>{title}</ArticleTitle>
{introPlainText && <Lead>{introPlainText}</Lead>}

View File

@@ -2,7 +2,6 @@ import { useState } from 'react'
import cx from 'classnames'
import { useRouter } from 'next/router'
import { MarkGithubIcon, ThreeBarsIcon, XIcon } from '@primer/octicons-react'
import { ButtonOutline } from '@primer/components'
import { Link } from 'components/Link'
import { useMainContext } from 'components/context/MainContext'
@@ -10,8 +9,8 @@ import { LanguagePicker } from './LanguagePicker'
import { HeaderNotifications } from 'components/page-header/HeaderNotifications'
import { ProductPicker } from 'components/page-header/ProductPicker'
import { useTranslation } from 'components/hooks/useTranslation'
import { HomepageVersionPicker } from 'components/landing/HomepageVersionPicker'
import { Search } from 'components/Search'
import { VersionPicker } from 'components/VersionPicker'
export const Header = () => {
const router = useRouter()
@@ -31,25 +30,23 @@ export const Header = () => {
<div className="border-bottom color-border-secondary no-print">
{error !== '404' && <HeaderNotifications />}
<header
className="container-xl px-3 px-md-6 pt-3 pb-3 position-relative"
style={{ zIndex: 2 }}
>
<header className={cx('container-xl px-3 px-md-6 pt-3 pb-3 position-relative z-3')}>
{/* desktop header */}
<div className="d-none d-lg-flex flex-justify-end" data-testid="desktop-header">
<div
className="d-none d-lg-flex flex-justify-end flex-items-center"
data-testid="desktop-header"
>
{showVersionPicker && (
<div className="py-2 mr-4">
<HomepageVersionPicker />
<div className="mr-2">
<VersionPicker hideLabel={true} variant="compact" />
</div>
)}
<div className="py-2">
<LanguagePicker />
</div>
<LanguagePicker />
{/* <!-- GitHub.com homepage and 404 page has a stylized search; Enterprise homepages do not --> */}
{relativePath !== 'index.md' && error !== '404' && (
<div className="d-inline-block ml-4">
<div className="d-inline-block ml-3">
<Search updateSearchParams={updateSearchParams} isOverlay={true} />
</div>
)}
@@ -72,14 +69,14 @@ export const Header = () => {
</div>
<div>
<ButtonOutline
<button
className="btn"
data-testid="mobile-menu-button"
css
onClick={() => setIsMenuOpen(!isMenuOpen)}
aria-label="Navigation Menu"
>
{isMenuOpen ? <XIcon size="small" /> : <ThreeBarsIcon size="small" />}
</ButtonOutline>
</button>
</div>
</div>
@@ -87,31 +84,33 @@ export const Header = () => {
<div className="relative">
<div
className={cx(
'width-full position-absolute left-0 right-0 color-shadow-large color-bg-primary px-3 px-md-6 pb-3',
'width-full position-absolute left-0 right-0 color-shadow-large color-bg-primary px-2 px-md-4 pb-3',
isMenuOpen ? 'd-block' : 'd-none'
)}
>
<div className="mt-3 mb-2">
<h4 className="f5 text-normal color-text-secondary">{t('explore_by_product')}</h4>
<h4 className="f5 text-normal color-text-secondary ml-3">
{t('explore_by_product')}
</h4>
<ProductPicker />
</div>
{/* <!-- Versions picker that only appears in the header on landing pages --> */}
{showVersionPicker && (
<div className="border-top py-2">
<HomepageVersionPicker variant="inline" />
</div>
<>
<div className="border-top my-2 mx-3" />
<VersionPicker hideLabel={true} variant="inline" popoverVariant={'inline'} />
</>
)}
{/* <!-- Language picker - 'English', 'Japanese', etc --> */}
<div className="border-top py-2">
<LanguagePicker variant="inline" />
</div>
<div className="border-top my-2 mx-3" />
<LanguagePicker variant="inline" />
{/* <!-- GitHub.com homepage and 404 page has a stylized search; Enterprise homepages do not --> */}
{relativePath !== 'index.md' && error !== '404' && (
<div className="pt-3 border-top">
<div className="my-2 pt-3 mx-3">
<Search updateSearchParams={updateSearchParams} />
</div>
)}

View File

@@ -1,6 +1,5 @@
import cx from 'classnames'
import { useRouter } from 'next/router'
import { Dropdown, Details, useDetails } from '@primer/components'
import { Box, Dropdown, Details, Text, useDetails } from '@primer/components'
import { ChevronDownIcon } from '@primer/octicons-react'
import { Link } from 'components/Link'
@@ -12,76 +11,63 @@ type Props = {
export const LanguagePicker = ({ variant }: Props) => {
const router = useRouter()
const { languages } = useLanguages()
const { getDetailsProps } = useDetails({})
const { getDetailsProps, setOpen } = useDetails({ closeOnOutsideClick: true })
const locale = router.locale || 'en'
const langs = Object.values(languages)
const selectedLang = languages[locale]
if (variant === 'inline') {
return (
<Details {...getDetailsProps()} className="details-reset">
<summary className="outline-none" aria-label="Toggle language list">
<div className="d-flex flex-items-center flex-justify-between py-2">
<span>{selectedLang.nativeName || selectedLang.name}</span>
<Details {...getDetailsProps()} data-testid="language-picker">
<summary
className="d-block btn btn-invisible color-text-primary"
aria-label="Toggle language list"
>
<div className="d-flex flex-items-center flex-justify-between">
<Text>{selectedLang.nativeName || selectedLang.name}</Text>
<ChevronDownIcon size={24} className="arrow ml-md-1" />
</div>
</summary>
<div>
<Box mt={1}>
{langs.map((lang) => {
if (lang.wip) {
return null
}
return (
<Link
key={lang.code}
href={router.asPath}
locale={lang.code}
disableClientTransition={true}
className={cx(
'd-block py-2',
lang.code === router.locale
? 'color-text-link text-underline active'
: 'Link--primary no-underline'
)}
>
{lang.nativeName ? (
<>
{lang.nativeName} ({lang.name})
</>
) : (
lang.name
)}
</Link>
<Dropdown.Item onClick={() => setOpen(false)} key={lang.code}>
<Link href={router.asPath} locale={lang.code}>
{lang.nativeName ? (
<>
{lang.nativeName} ({lang.name})
</>
) : (
lang.name
)}
</Link>
</Dropdown.Item>
)
})}
</div>
</Box>
</Details>
)
}
return (
<Dropdown
css={`
ul {
width: unset;
}
`}
data-testid="language-picker"
>
<summary>
{selectedLang.nativeName || selectedLang.name}
<Details {...getDetailsProps()} data-testid="language-picker" className="position-relative">
<summary className="d-block btn btn-invisible color-text-primary">
<Text>{selectedLang.nativeName || selectedLang.name}</Text>
<Dropdown.Caret />
</summary>
<Dropdown.Menu direction="sw">
<Dropdown.Menu direction="sw" style={{ width: 'unset' }}>
{langs.map((lang) => {
if (lang.wip) {
return null
}
return (
<Dropdown.Item key={lang.code}>
<Link href={router.asPath} locale={lang.code} disableClientTransition={true}>
<Dropdown.Item key={lang.code} onClick={() => setOpen(false)}>
<Link href={router.asPath} locale={lang.code}>
{lang.nativeName ? (
<>
{lang.nativeName} ({lang.name})
@@ -94,6 +80,6 @@ export const LanguagePicker = ({ variant }: Props) => {
)
})}
</Dropdown.Menu>
</Dropdown>
</Details>
)
}

View File

@@ -1,53 +1,48 @@
import { useRouter } from 'next/router'
import cx from 'classnames'
import { Link } from 'components/Link'
import { useMainContext } from 'components/context/MainContext'
import { ChevronDownIcon, LinkExternalIcon } from '@primer/octicons-react'
import { Details, useDetails } from '@primer/components'
import { Box, Dropdown, Details, useDetails } from '@primer/components'
// Product Picker - GitHub.com, Enterprise Server, etc
export const ProductPicker = () => {
const router = useRouter()
const { activeProducts, currentProduct } = useMainContext()
const { getDetailsProps } = useDetails({})
const { getDetailsProps, setOpen } = useDetails({ closeOnOutsideClick: true })
return (
<Details {...getDetailsProps()} className="details-reset">
<summary
className="color-text-link outline-none"
className="d-block color-text-primary btn btn-invisible"
role="button"
aria-label="Toggle products list"
>
<div id="current-product" className="d-flex flex-items-center flex-justify-between py-2">
{/* <!-- Product switcher - GitHub.com, Enterprise Server, etc -->
<!-- 404 and 500 error layouts are not real pages so we need to hardcode the name for those --> */}
<div
data-testid="current-product"
data-current-product-path={currentProduct?.href}
className="d-flex flex-items-center flex-justify-between"
>
<span>{currentProduct?.name || 'All Products'}</span>
<ChevronDownIcon size={24} className="arrow ml-md-1" />
</div>
</summary>
<div id="homepages" style={{ zIndex: 6 }}>
<Box data-testid="product-picker-list" py="2" style={{ zIndex: 6 }}>
{activeProducts.map((product) => {
return (
<Link
key={product.id}
href={`${product.external ? '' : `/${router.locale}`}${product.href}`}
className={cx(
'd-block py-2',
product.id === currentProduct?.id
? 'color-text-link text-underline active'
: 'Link--primary no-underline'
)}
>
{product.name}
{product.external && (
<span className="ml-1">
<LinkExternalIcon size="small" />
</span>
)}
</Link>
<Dropdown.Item key={product.id} onClick={() => setOpen(false)}>
<Link href={`${product.external ? '' : `/${router.locale}`}${product.href}`}>
{product.name}
{product.external && (
<span className="ml-1">
<LinkExternalIcon size="small" />
</span>
)}
</Link>
</Dropdown.Item>
)
})}
</div>
</Box>
</Details>
)
}

View File

@@ -31,17 +31,16 @@ export const CodeLanguagePicker = ({ variant }: Props) => {
}
return (
<SelectMenu css className="position-relative">
<Button as="summary" css>
<SelectMenu className="position-relative">
<Button as="summary">
{currentLanguage.label} <Dropdown.Caret />
</Button>
<SelectMenu.Modal css style={{ minWidth: 300 }} align="right">
<SelectMenu.Header css>Programming Language</SelectMenu.Header>
<SelectMenu.Modal style={{ minWidth: 300 }} align="right">
<SelectMenu.Header>Programming Language</SelectMenu.Header>
<SelectMenu.List>
{codeLanguages.map((language) => (
<SelectMenu.Item
key={language.id}
css
as="a"
href={`${routePath}?langId=${language.id}`}
selected={language.id === currentLanguage.id}

View File

@@ -40,9 +40,10 @@ toc:
guides: Guides
whats_new: What's new
pages:
article_version: 'Article version:'
article_version: 'Article version'
miniToc: In this article
contributor_callout: This article is contributed and maintained by
all_enterprise_releases: All Enterprise releases
errors:
oops: Ooops!
something_went_wrong: It looks like something went wrong.

412
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@
"dependencies": {
"@alex_neo/jest-expect-message": "^1.0.5",
"@hapi/accept": "^5.0.2",
"@primer/components": "^28.5.0",
"@primer/components": "^29.1.1",
"@primer/css": "^17.9.0",
"@primer/octicons": "^15.1.0",
"@primer/octicons-react": "^15.1.0",

View File

@@ -68,6 +68,9 @@
.z-2 {
z-index: 2;
}
.z-3 {
z-index: 3;
}
/* Blue primary button
------------------------------------------------------------------------------*/

View File

@@ -7,7 +7,7 @@ describe('header', () => {
test('includes localized meta tags', async () => {
const $ = await getDOM('/en')
expect($('meta[name="next-head-count"]').length).toBe(1)
expect($('link[rel="alternate"]').length).toBeGreaterThan(2)
})
test("includes a link to the homepage (in the current page's language)", async () => {
@@ -26,26 +26,30 @@ describe('header', () => {
)
expect(
$(
'[data-testid=language-picker] a[href="/ja/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/managing-a-branch-protection-rule"]'
'[data-testid=desktop-header] [data-testid=language-picker] a[href="/ja/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/managing-a-branch-protection-rule"]'
).length
).toBe(1)
})
test('display the native name and the English name for each translated language', async () => {
const $ = await getDOM('/en')
expect($('[data-testid=language-picker] a[href="/en/"]').text().trim()).toBe('English')
expect($('[data-testid=language-picker] a[href="/cn/"]').text().trim()).toBe(
'简体中文 (Simplified Chinese)'
)
expect($('[data-testid=language-picker] a[href="/ja/"]').text().trim()).toBe(
'日本語 (Japanese)'
)
expect(
$('[data-testid=desktop-header] [data-testid=language-picker] a[href="/en"]').text().trim()
).toBe('English')
expect(
$('[data-testid=desktop-header] [data-testid=language-picker] a[href="/cn"]').text().trim()
).toBe('简体中文 (Simplified Chinese)')
expect(
$('[data-testid=desktop-header] [data-testid=language-picker] a[href="/ja"]').text().trim()
).toBe('日本語 (Japanese)')
})
test('emphasize the current language', async () => {
const $ = await getDOM('/en')
expect($('[data-testid=language-picker] a[href="/en/"]').length).toBe(1)
expect($('[data-testid=language-picker] a[href="/ja/"]').length).toBe(1)
expect($('[data-testid=desktop-header] [data-testid=language-picker] summary').text()).toBe(
'English'
)
})
})
@@ -136,15 +140,15 @@ describe('header', () => {
const $ = await getDOM(
'/en/github/importing-your-projects-to-github/importing-source-code-to-github/about-github-importer'
)
const github = $('#homepages a.active[href="/en/github"]')
const github = $('[data-testid=current-product][data-current-product-path="/github"]')
expect(github.length).toBe(1)
expect(github.text().trim()).toBe('GitHub')
expect(github.attr('class').includes('active')).toBe(true)
const ghe = $(`#homepages a[href="/en/enterprise-server@${latest}/admin"]`)
const ghe = $(
`[data-testid=product-picker-list] a[href="/en/enterprise-server@${latest}/admin"]`
)
expect(ghe.length).toBe(1)
expect(ghe.text().trim()).toBe('Enterprise administrators')
expect(ghe.attr('class').includes('active')).toBe(false)
})
// Skipped. See issues/923
@@ -152,17 +156,21 @@ describe('header', () => {
const $ = await getDOM(
'/ja/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests'
)
expect($('#homepages a.active[href="/ja/repositories"]').length).toBe(1)
expect($(`#homepages a[href="/ja/enterprise-server@${latest}/admin"]`).length).toBe(1)
expect(
$('[data-testid=current-product][data-current-product-path="/repositories"]').length
).toBe(1)
expect(
$(`[data-testid=product-picker-list] a[href="/ja/enterprise-server@${latest}/admin"]`)
.length
).toBe(1)
})
test('emphasizes the product that corresponds to the current page', async () => {
const $ = await getDOM(
`/en/enterprise/${oldestSupported}/user/github/importing-your-projects-to-github/importing-source-code-to-github/importing-a-git-repository-using-the-command-line`
`/en/enterprise-server@${oldestSupported}/github/importing-your-projects-to-github/importing-source-code-to-github/importing-a-git-repository-using-the-command-line`
)
expect($(`#homepages a.active[href="/en/enterprise-server@${latest}/admin"]`).length).toBe(0)
expect($('#homepages a[href="/en/github"]').length).toBe(1)
expect($('#homepages a.active[href="/en/github"]').length).toBe(1)
expect($('[data-testid=current-product]').text()).toBe('GitHub')
})
})
})

View File

@@ -545,7 +545,7 @@ describe('server', () => {
$(
`[data-testid=article-version-picker] a[href="/en/enterprise-server@${enterpriseServerReleases.latest}/${articlePath}"]`
).length
).toBe(2)
).toBe(1)
// 2.13 predates this feature, so it should be excluded:
expect(
$(`[data-testid=article-version-picker] a[href="/en/enterprise/2.13/user/${articlePath}"]`)

View File

@@ -25,28 +25,34 @@ describe('products module', () => {
describe('mobile-only products nav', () => {
test('renders current product on various product pages for each product', async () => {
// Note the unversioned homepage at `/` does not have a product selected in the mobile dropdown
expect((await getDOM('/github'))('#current-product').text().trim()).toBe('GitHub')
expect((await getDOM('/github'))('[data-testid=current-product]').text().trim()).toBe('GitHub')
// Enterprise server
expect((await getDOM('/en/enterprise/admin'))('#current-product').text().trim()).toBe(
'Enterprise administrators'
)
expect(
(await getDOM('/en/enterprise/admin'))('[data-testid=current-product]').text().trim()
).toBe('Enterprise administrators')
expect(
(
await getDOM(
'/en/enterprise/user/github/importing-your-projects-to-github/importing-source-code-to-github/importing-a-git-repository-using-the-command-line'
)
)('#current-product')
)('[data-testid=current-product]')
.text()
.trim()
).toBe('GitHub')
expect((await getDOM('/desktop'))('#current-product').text().trim()).toBe('GitHub Desktop')
expect((await getDOM('/desktop'))('[data-testid=current-product]').text().trim()).toBe(
'GitHub Desktop'
)
expect((await getDOM('/actions'))('#current-product').text().trim()).toBe('GitHub Actions')
expect((await getDOM('/actions'))('[data-testid=current-product]').text().trim()).toBe(
'GitHub Actions'
)
// localized
expect((await getDOM('/ja/desktop'))('#current-product').text().trim()).toBe('GitHub Desktop')
expect((await getDOM('/ja/desktop'))('[data-testid=current-product]').text().trim()).toBe(
'GitHub Desktop'
)
})
})