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:
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
126
components/VersionPicker.tsx
Normal file
126
components/VersionPicker.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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')};
|
||||
}
|
||||
`
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>}
|
||||
|
||||
@@ -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>
|
||||
|
||||
{/* <!-- 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">
|
||||
<div className="border-top my-2 mx-3" />
|
||||
<LanguagePicker variant="inline" />
|
||||
</div>
|
||||
|
||||
{/* <!-- 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>
|
||||
)}
|
||||
|
||||
@@ -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,39 +11,32 @@ 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'
|
||||
)}
|
||||
>
|
||||
<Dropdown.Item onClick={() => setOpen(false)} key={lang.code}>
|
||||
<Link href={router.asPath} locale={lang.code}>
|
||||
{lang.nativeName ? (
|
||||
<>
|
||||
{lang.nativeName} ({lang.name})
|
||||
@@ -53,35 +45,29 @@ export const LanguagePicker = ({ variant }: Props) => {
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,43 +1,37 @@
|
||||
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'
|
||||
)}
|
||||
>
|
||||
<Dropdown.Item key={product.id} onClick={() => setOpen(false)}>
|
||||
<Link href={`${product.external ? '' : `/${router.locale}`}${product.href}`}>
|
||||
{product.name}
|
||||
{product.external && (
|
||||
<span className="ml-1">
|
||||
@@ -45,9 +39,10 @@ export const ProductPicker = () => {
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</Dropdown.Item>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Box>
|
||||
</Details>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
412
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -68,6 +68,9 @@
|
||||
.z-2 {
|
||||
z-index: 2;
|
||||
}
|
||||
.z-3 {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
/* Blue primary button
|
||||
------------------------------------------------------------------------------*/
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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}"]`)
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user