@@ -22,16 +22,15 @@ import styles from './Header.module.scss'
|
||||
export const Header = () => {
|
||||
const router = useRouter()
|
||||
const { error } = useMainContext()
|
||||
const { allVersions } = useMainContext()
|
||||
const { currentProduct, allVersions } = useMainContext()
|
||||
const { currentVersion } = useVersion()
|
||||
const { t } = useTranslation(['header', 'homepage'])
|
||||
const isRestPage = currentProduct && currentProduct.id === 'rest'
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(
|
||||
router.pathname !== '/' && router.query.query && true
|
||||
)
|
||||
const [scroll, setScroll] = useState(false)
|
||||
|
||||
const { hasAccount } = useHasAccount()
|
||||
|
||||
const signupCTAVisible =
|
||||
hasAccount === false && // don't show if `null`
|
||||
(currentVersion === DEFAULT_VERSION || currentVersion === 'enterprise-cloud@latest')
|
||||
@@ -92,8 +91,8 @@ export const Header = () => {
|
||||
<Breadcrumbs />
|
||||
</div>
|
||||
<div className="d-flex flex-items-center">
|
||||
<VersionPicker />
|
||||
<LanguagePicker />
|
||||
<VersionPicker variant="header" />
|
||||
<LanguagePicker variant="header" />
|
||||
|
||||
{signupCTAVisible && (
|
||||
<a
|
||||
@@ -161,7 +160,7 @@ export const Header = () => {
|
||||
<div className="border-top my-2" />
|
||||
<LanguagePicker variant="inline" />
|
||||
|
||||
{allVersions[currentVersion].apiVersions.length > 0 && (
|
||||
{isRestPage && allVersions[currentVersion].apiVersions.length > 0 && (
|
||||
<ApiVersionPicker variant="inline" />
|
||||
)}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import { useRouter } from 'next/router'
|
||||
import Cookies from 'js-cookie'
|
||||
|
||||
import { useLanguages } from 'components/context/LanguagesContext'
|
||||
import { Picker } from 'components/ui/Picker'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
import { Picker } from 'components/ui/Picker'
|
||||
import { USER_LANGUAGE_COOKIE_NAME } from '../../lib/constants.js'
|
||||
|
||||
function rememberPreferredLanguage(value: string) {
|
||||
@@ -31,7 +31,7 @@ function rememberPreferredLanguage(value: string) {
|
||||
}
|
||||
|
||||
type Props = {
|
||||
variant?: 'inline'
|
||||
variant: 'inline' | 'header'
|
||||
}
|
||||
|
||||
export const LanguagePicker = ({ variant }: Props) => {
|
||||
@@ -61,13 +61,20 @@ export const LanguagePicker = ({ variant }: Props) => {
|
||||
<Picker
|
||||
variant={variant}
|
||||
defaultText={t('language_picker_default_text')}
|
||||
options={langs.map((lang) => ({
|
||||
items={langs.map((lang) => ({
|
||||
text: lang.nativeName || lang.name,
|
||||
selected: lang === selectedLang,
|
||||
locale: lang.code,
|
||||
href: `${routerPath}`,
|
||||
onselect: rememberPreferredLanguage,
|
||||
href: `/${lang.code}${routerPath}`,
|
||||
extra: {
|
||||
locale: lang.code,
|
||||
},
|
||||
}))}
|
||||
onSelect={(item) => {
|
||||
if (item.extra?.locale) rememberPreferredLanguage(item.extra.locale)
|
||||
}}
|
||||
dataTestId="field"
|
||||
ariaLabel="Select field type"
|
||||
alignment="center"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { LinkExternalIcon } from '@primer/octicons-react'
|
||||
|
||||
import { useMainContext } from 'components/context/MainContext'
|
||||
import { Picker } from 'components/ui/Picker'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
import { Picker } from 'components/ui/Picker'
|
||||
|
||||
export const ProductPicker = () => {
|
||||
const router = useRouter()
|
||||
@@ -14,12 +15,27 @@ export const ProductPicker = () => {
|
||||
<Picker
|
||||
variant="inline"
|
||||
defaultText={t('product_picker_default_text')}
|
||||
options={activeProducts.map((product) => ({
|
||||
items={activeProducts.map((product) => ({
|
||||
text: product.name,
|
||||
selected: product.name === currentProduct?.name,
|
||||
external: product.external,
|
||||
href: `${product.external ? '' : `/${router.locale}`}${product.href}`,
|
||||
extra: {
|
||||
external: product.external,
|
||||
},
|
||||
}))}
|
||||
alignment="end"
|
||||
dataTestId="field"
|
||||
ariaLabel="Select field type"
|
||||
renderItem={(item) => {
|
||||
return item.extra?.external ? (
|
||||
<>
|
||||
{item.text}
|
||||
<LinkExternalIcon size="small" className="ml-1" />
|
||||
</>
|
||||
) : (
|
||||
item.text
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { ArrowRightIcon, InfoIcon } from '@primer/octicons-react'
|
||||
|
||||
import { useMainContext } from 'components/context/MainContext'
|
||||
import { DEFAULT_VERSION, useVersion } from 'components/hooks/useVersion'
|
||||
@@ -6,7 +7,7 @@ import { useTranslation } from 'components/hooks/useTranslation'
|
||||
import { Picker } from 'components/ui/Picker'
|
||||
|
||||
type Props = {
|
||||
variant?: 'inline'
|
||||
variant: 'inline' | 'header'
|
||||
}
|
||||
|
||||
export const VersionPicker = ({ variant }: Props) => {
|
||||
@@ -23,8 +24,10 @@ export const VersionPicker = ({ variant }: Props) => {
|
||||
text: allVersions[permalink.pageVersion].versionTitle,
|
||||
selected: currentVersion === permalink.pageVersion,
|
||||
href: permalink.href,
|
||||
arrow: false,
|
||||
info: false,
|
||||
extra: {
|
||||
arrow: false,
|
||||
info: false,
|
||||
},
|
||||
}))
|
||||
|
||||
const hasEnterpriseVersions = (page.permalinks || []).some((permalink) =>
|
||||
@@ -35,9 +38,11 @@ export const VersionPicker = ({ variant }: Props) => {
|
||||
allLinks.push({
|
||||
text: t('all_enterprise_releases'),
|
||||
selected: false,
|
||||
arrow: true,
|
||||
href: `/${router.locale}/${enterpriseServerVersions[0]}/admin/all-releases`,
|
||||
info: false,
|
||||
extra: {
|
||||
arrow: true,
|
||||
info: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -47,15 +52,35 @@ export const VersionPicker = ({ variant }: Props) => {
|
||||
allLinks.push({
|
||||
text: t('about_versions'),
|
||||
selected: false,
|
||||
arrow: false,
|
||||
info: true,
|
||||
href: `/${router.locale}${currentVersionPathSegment}/get-started/learning-about-github/about-versions-of-github-docs`,
|
||||
extra: {
|
||||
arrow: false,
|
||||
info: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-testid="version-picker">
|
||||
<Picker variant={variant} defaultText={t('version_picker_default_text')} options={allLinks} />
|
||||
<Picker
|
||||
variant={variant}
|
||||
defaultText={t('version_picker_default_text')}
|
||||
items={allLinks}
|
||||
alignment="end"
|
||||
dataTestId="field"
|
||||
ariaLabel="Select field type"
|
||||
renderItem={(item) => {
|
||||
return (
|
||||
<div className={item.extra?.arrow || item.extra?.info ? 'f6' : undefined}>
|
||||
{item.text}
|
||||
{item.extra?.arrow && (
|
||||
<ArrowRightIcon verticalAlign="middle" size={15} className="ml-1" />
|
||||
)}
|
||||
{item.extra?.info && <InfoIcon verticalAlign="middle" size={15} className="ml-1" />}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import cx from 'classnames'
|
||||
import Cookies from 'js-cookie'
|
||||
import { InfoIcon } from '@primer/octicons-react'
|
||||
|
||||
import { useMainContext } from 'components/context/MainContext'
|
||||
import { DEFAULT_VERSION, useVersion } from 'components/hooks/useVersion'
|
||||
@@ -13,7 +14,7 @@ import styles from './SidebarProduct.module.scss'
|
||||
const API_VERSION_SUFFIX = ' (latest)'
|
||||
|
||||
type Props = {
|
||||
variant?: 'inline'
|
||||
variant: 'inline' | 'header'
|
||||
width?: number
|
||||
}
|
||||
|
||||
@@ -67,19 +68,23 @@ export const ApiVersionPicker = ({ variant, width }: Props) => {
|
||||
text: dateDisplayText,
|
||||
selected: router.query.apiVersion === date,
|
||||
href: itemLink,
|
||||
info: false,
|
||||
onselect: rememberApiVersion,
|
||||
extra: {
|
||||
info: false,
|
||||
currentDate,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
apiVersionLinks.push({
|
||||
text: t('rest.versioning.about_versions'),
|
||||
selected: false,
|
||||
info: true,
|
||||
href: `/${router.locale}${
|
||||
currentVersion === DEFAULT_VERSION ? '' : `/${currentVersion}`
|
||||
}/rest/overview/api-versions`,
|
||||
onselect: rememberApiVersion,
|
||||
extra: {
|
||||
info: true,
|
||||
currentDate,
|
||||
},
|
||||
})
|
||||
|
||||
// This only shows the REST Version picker if it's calendar date versioned
|
||||
@@ -99,9 +104,26 @@ export const ApiVersionPicker = ({ variant, width }: Props) => {
|
||||
<div data-testid="api-version-picker" className="width-full">
|
||||
<Picker
|
||||
variant={variant}
|
||||
apiVersion={true}
|
||||
defaultText={currentDateDisplayText}
|
||||
options={apiVersionLinks}
|
||||
items={apiVersionLinks}
|
||||
pickerLabel="Version"
|
||||
alignment="center"
|
||||
buttonBorder={true}
|
||||
dataTestId="version"
|
||||
ariaLabel="Select API Version"
|
||||
onSelect={(item) => {
|
||||
if (item.extra?.currentDate) rememberApiVersion(item.extra.currentDate)
|
||||
}}
|
||||
renderItem={(item) => {
|
||||
return item.extra?.info ? (
|
||||
<div className="f6">
|
||||
{item.text}
|
||||
<InfoIcon verticalAlign="middle" size={15} className="ml-1" />
|
||||
</div>
|
||||
) : (
|
||||
item.text
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -109,7 +109,7 @@ export const SidebarProduct = () => {
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<ApiVersionPicker width={sidebarWidth} />
|
||||
<ApiVersionPicker width={sidebarWidth} variant="header" />
|
||||
<li className="my-3">
|
||||
<ul className="list-style-none">
|
||||
{conceptualPages.map((childPage, i) => {
|
||||
|
||||
33
components/ui/Picker/Fields.tsx
Normal file
33
components/ui/Picker/Fields.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { ActionList } from '@primer/react'
|
||||
|
||||
import { PickerItem } from './Picker'
|
||||
import { Link } from 'components/Link'
|
||||
|
||||
export const Fields = (fieldProps: {
|
||||
open: boolean
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||
items: PickerItem[]
|
||||
onSelect?: (item: PickerItem) => void
|
||||
renderItem?: (item: PickerItem) => ReactNode | string
|
||||
}) => {
|
||||
const { open, setOpen, items, onSelect, renderItem } = fieldProps
|
||||
|
||||
return (
|
||||
<ActionList selectionVariant="single">
|
||||
{items.map((item) => (
|
||||
<ActionList.LinkItem
|
||||
as={Link}
|
||||
key={item.text}
|
||||
href={item.href}
|
||||
onClick={() => {
|
||||
if (onSelect) onSelect(item)
|
||||
setOpen(!open)
|
||||
}}
|
||||
>
|
||||
{renderItem ? renderItem(item) : item.text}
|
||||
</ActionList.LinkItem>
|
||||
))}
|
||||
</ActionList>
|
||||
)
|
||||
}
|
||||
@@ -1,105 +1,90 @@
|
||||
import React, { useState } from 'react'
|
||||
import { ActionList, ActionMenu, Box, Details, Text, useDetails } from '@primer/react'
|
||||
import { ArrowRightIcon, ChevronDownIcon, InfoIcon, LinkExternalIcon } from '@primer/octicons-react'
|
||||
import React, { ReactNode, useState } from 'react'
|
||||
import cx from 'classnames'
|
||||
import { ActionMenu, Box, Details, Text, useDetails } from '@primer/react'
|
||||
import { ChevronDownIcon } from '@primer/octicons-react'
|
||||
import { AnchorAlignment } from '@primer/behaviors'
|
||||
|
||||
import { Link } from 'components/Link'
|
||||
import { Fields } from './Fields'
|
||||
|
||||
export type PickerOptionsTypeT = {
|
||||
text: string
|
||||
href: string
|
||||
locale?: string
|
||||
external?: boolean
|
||||
arrow?: boolean
|
||||
info?: boolean
|
||||
selected?: boolean
|
||||
onselect?: Function | void
|
||||
}
|
||||
|
||||
export type PickerPropsT = {
|
||||
variant?: 'inline'
|
||||
apiVersion?: boolean
|
||||
interface Props {
|
||||
variant: 'inline' | 'header'
|
||||
items: PickerItem[]
|
||||
onSelect?: (item: PickerItem) => void
|
||||
buttonBorder?: boolean
|
||||
pickerLabel?: string
|
||||
dataTestId: string
|
||||
defaultText: string
|
||||
options: Array<PickerOptionsTypeT>
|
||||
ariaLabel: string
|
||||
alignment: AnchorAlignment
|
||||
renderItem?: (item: PickerItem) => ReactNode | string
|
||||
}
|
||||
|
||||
export function Picker({ variant, apiVersion, defaultText, options }: PickerPropsT) {
|
||||
export interface PickerItem {
|
||||
href: string
|
||||
text: string
|
||||
selected: boolean
|
||||
extra?: {
|
||||
[key: string]: any
|
||||
}
|
||||
}
|
||||
|
||||
export const Picker = ({
|
||||
variant,
|
||||
items,
|
||||
ariaLabel,
|
||||
pickerLabel,
|
||||
dataTestId,
|
||||
defaultText,
|
||||
onSelect,
|
||||
buttonBorder,
|
||||
alignment,
|
||||
renderItem,
|
||||
}: Props) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const { getDetailsProps } = useDetails({ closeOnOutsideClick: true })
|
||||
const selectedOption = options.find((opt) => opt.selected === true)
|
||||
const selectedOption = items.find((item) => item.selected === true)
|
||||
|
||||
function getFields() {
|
||||
return (
|
||||
<ActionList selectionVariant="single">
|
||||
{options.map((option) => (
|
||||
<ActionList.LinkItem
|
||||
as={Link}
|
||||
className={option.arrow || option.info ? 'f6' : ''}
|
||||
locale={option.locale}
|
||||
key={option.text}
|
||||
href={option.href}
|
||||
onClick={() => {
|
||||
if (option.onselect) {
|
||||
if (apiVersion) {
|
||||
option.onselect(option.text)
|
||||
} else {
|
||||
option.onselect(option.locale)
|
||||
}
|
||||
}
|
||||
setOpen(!open)
|
||||
}}
|
||||
>
|
||||
{option.text}
|
||||
{option.external && <LinkExternalIcon size="small" className="ml-1" />}
|
||||
{option.info && <InfoIcon verticalAlign="middle" size={15} className="ml-1" />}
|
||||
{option.arrow && <ArrowRightIcon verticalAlign="middle" size={15} className="ml-1" />}
|
||||
</ActionList.LinkItem>
|
||||
))}
|
||||
</ActionList>
|
||||
)
|
||||
}
|
||||
|
||||
function getInlinePicker() {
|
||||
return (
|
||||
<Details {...getDetailsProps()} className={cx('position-relative details-reset', 'd-block')}>
|
||||
<summary
|
||||
className="d-block btn btn-invisible color-fg-default"
|
||||
aria-haspopup="true"
|
||||
aria-label={selectedOption?.text || defaultText}
|
||||
>
|
||||
<div className="d-flex flex-items-center flex-justify-between">
|
||||
<Text>{selectedOption?.text || defaultText}</Text>
|
||||
<ChevronDownIcon size={24} className="arrow ml-md-1" />
|
||||
</div>
|
||||
</summary>
|
||||
<Box>
|
||||
<ul>{getFields()}</ul>
|
||||
</Box>
|
||||
</Details>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{variant === 'inline' ? (
|
||||
getInlinePicker()
|
||||
) : (
|
||||
<ActionMenu open={open} onOpenChange={setOpen}>
|
||||
<ActionMenu.Button
|
||||
aria-label={apiVersion ? `Select API version` : `Select field type`}
|
||||
variant={apiVersion ? 'default' : 'invisible'}
|
||||
sx={{ color: `var(--color-fg-default)`, width: '100%' }}
|
||||
>
|
||||
<span style={{ fontWeight: 'normal' }}>{`${apiVersion ? `Version: ` : ''}`}</span>
|
||||
<span data-testid={apiVersion ? `version` : `field`}>
|
||||
{selectedOption?.text || defaultText}
|
||||
</span>
|
||||
</ActionMenu.Button>
|
||||
<ActionMenu.Overlay width="auto" align={apiVersion ? 'center' : 'end'}>
|
||||
{getFields()}
|
||||
</ActionMenu.Overlay>
|
||||
</ActionMenu>
|
||||
)}
|
||||
</React.Fragment>
|
||||
return variant === 'inline' ? (
|
||||
<Details {...getDetailsProps()} className={cx('position-relative details-reset', 'd-block')}>
|
||||
<summary
|
||||
className="d-block btn btn-invisible color-fg-default"
|
||||
aria-haspopup="true"
|
||||
aria-label={selectedOption?.text || defaultText}
|
||||
>
|
||||
<div className="d-flex flex-items-center flex-justify-between">
|
||||
<Text>{selectedOption?.text || defaultText}</Text>
|
||||
<ChevronDownIcon size={24} className="arrow ml-md-1" />
|
||||
</div>
|
||||
</summary>
|
||||
<Box>
|
||||
<Fields
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
items={items}
|
||||
onSelect={onSelect}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
</Box>
|
||||
</Details>
|
||||
) : (
|
||||
<ActionMenu open={open} onOpenChange={setOpen}>
|
||||
<ActionMenu.Button
|
||||
aria-label={ariaLabel}
|
||||
variant={buttonBorder ? 'default' : 'invisible'}
|
||||
sx={{ color: `var(--color-fg-default)`, width: '100%' }}
|
||||
>
|
||||
{pickerLabel && <span style={{ fontWeight: 'normal' }}>{`${pickerLabel}: `}</span>}
|
||||
<span data-testid={dataTestId}>{selectedOption?.text || defaultText}</span>
|
||||
</ActionMenu.Button>
|
||||
<ActionMenu.Overlay width="auto" align={alignment}>
|
||||
<Fields
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
items={items}
|
||||
onSelect={onSelect}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
</ActionMenu.Overlay>
|
||||
</ActionMenu>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { Picker } from './Picker'
|
||||
export { Fields } from './Fields'
|
||||
|
||||
13
package-lock.json
generated
13
package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"dependencies": {
|
||||
"@elastic/elasticsearch": "7.11.0",
|
||||
"@github/failbot": "0.8.0",
|
||||
"@primer/behaviors": "^1.3.1",
|
||||
"@primer/css": "^20.2.4",
|
||||
"@primer/octicons": "17.7.0",
|
||||
"@primer/octicons-react": "17.7.0",
|
||||
@@ -4051,9 +4052,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@primer/behaviors": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@primer/behaviors/-/behaviors-1.1.3.tgz",
|
||||
"integrity": "sha512-WpCcjAkXG7Lv3ZbaCUgASWKHnCi/pmuSEiyTmHHb6f5xhwk1mliixNL5ZZHtDN6RCcT3VnXUsyek4GopG2lbZQ=="
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@primer/behaviors/-/behaviors-1.3.1.tgz",
|
||||
"integrity": "sha512-aMRDUQ350lk0FxtL5gJWPFHHOSSzDbJ6uNJVIt8XSqiGe1pxuW5mVVfrEp1uvzZ0pCHkCdm9fycjnfOeMeIrOQ=="
|
||||
},
|
||||
"node_modules/@primer/css": {
|
||||
"version": "20.2.4",
|
||||
@@ -23127,9 +23128,9 @@
|
||||
}
|
||||
},
|
||||
"@primer/behaviors": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@primer/behaviors/-/behaviors-1.1.3.tgz",
|
||||
"integrity": "sha512-WpCcjAkXG7Lv3ZbaCUgASWKHnCi/pmuSEiyTmHHb6f5xhwk1mliixNL5ZZHtDN6RCcT3VnXUsyek4GopG2lbZQ=="
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@primer/behaviors/-/behaviors-1.3.1.tgz",
|
||||
"integrity": "sha512-aMRDUQ350lk0FxtL5gJWPFHHOSSzDbJ6uNJVIt8XSqiGe1pxuW5mVVfrEp1uvzZ0pCHkCdm9fycjnfOeMeIrOQ=="
|
||||
},
|
||||
"@primer/css": {
|
||||
"version": "20.2.4",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@elastic/elasticsearch": "7.11.0",
|
||||
"@github/failbot": "0.8.0",
|
||||
"@primer/behaviors": "^1.3.1",
|
||||
"@primer/css": "^20.2.4",
|
||||
"@primer/octicons": "17.7.0",
|
||||
"@primer/octicons-react": "17.7.0",
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('header', () => {
|
||||
const $ = await getDOM(
|
||||
'/en/get-started/importing-your-projects-to-github/importing-source-code-to-github/about-github-importer'
|
||||
)
|
||||
const getStarted = $('div ul ul li a[href="/en/get-started"]')
|
||||
const getStarted = $('details div li a[href="/en/get-started"]')
|
||||
expect(getStarted.length).toBe(1)
|
||||
expect(getStarted.text().trim()).toBe('Get started')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user