1
0
mirror of synced 2026-01-07 00:01:39 -05:00

Merge pull request #27229 from github/repo-sync

Repo sync
This commit is contained in:
docs-bot
2023-08-02 09:58:40 -07:00
committed by GitHub
8 changed files with 196 additions and 115 deletions

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import cx from 'classnames'
import { useRouter } from 'next/router'
import { AnchoredOverlay, Button, Dialog, IconButton } from '@primer/react'
import { ActionList, ActionMenu, Dialog, IconButton } from '@primer/react'
import {
KebabHorizontalIcon,
LinkExternalIcon,
@@ -30,16 +30,13 @@ import styles from './Header.module.scss'
export const Header = () => {
const router = useRouter()
const { error } = useMainContext()
const { isHomepageVersion, currentProduct, allVersions } = useMainContext()
const { isHomepageVersion, currentProduct } = useMainContext()
const { currentVersion } = useVersion()
const { t } = useTranslation(['header'])
const isRestPage = currentProduct && currentProduct.id === 'rest'
const [isSearchOpen, setIsSearchOpen] = useState(false)
const [scroll, setScroll] = useState(false)
const { hasAccount } = useHasAccount()
const [isMenuOpen, setIsMenuOpen] = useState(false)
const openMenuOverlay = useCallback(() => setIsMenuOpen(true), [setIsMenuOpen])
const closeMenuOverlay = useCallback(() => setIsMenuOpen(false), [setIsMenuOpen])
const [isSidebarOpen, setIsSidebarOpen] = useState(false)
const openSidebar = useCallback(() => setIsSidebarOpen(true), [isSidebarOpen])
const closeSidebar = useCallback(() => setIsSidebarOpen(false), [isSidebarOpen])
@@ -230,15 +227,12 @@ export const Header = () => {
{/* The ... navigation menu at medium and smaller widths */}
<div>
<AnchoredOverlay
anchorRef={menuButtonRef}
renderAnchor={(anchorProps) => (
<Button
<ActionMenu aria-labelledby="menu-title">
<ActionMenu.Anchor>
<IconButton
data-testid="mobile-menu"
className="px-2"
{...anchorProps}
icon={KebabHorizontalIcon}
aria-label="Open Menu Bar"
aria-label="Open Menu"
sx={
isSearchOpen
? // The ... menu button when the smaller width search UI is open. Since the search
@@ -271,47 +265,56 @@ export const Header = () => {
},
}
}
/>
</ActionMenu.Anchor>
<ActionMenu.Overlay align="start">
{/* Mobile Menu at XS browser width */}
<ActionList
sx={{
'@media (min-width: 544px)': {
display: 'none',
},
}}
>
{}
</Button>
)}
open={isMenuOpen}
onOpen={openMenuOverlay}
onClose={closeMenuOverlay}
aria-labelledby="menu-title"
>
<div
data-testid="open-mobile-menu"
className={cx('pt-2', !signupCTAVisible && 'pb-2', styles.menuOverlay)}
>
<span id="menu-title" className="f6 px-3 py-2 mb-1 d-block h6 color-fg-muted">
{t('menu')}
</span>
<span className="px-2 pb-2 m-2 d-block d-sm-none">
<VersionPicker mediumOrLower={true} />
</span>
<span className="px-2 pb-2 m-2 d-block">
<LanguagePicker mediumOrLower={true} />
</span>
{isRestPage && allVersions[currentVersion].apiVersions.length > 0 && (
<span className="px-2 pb-2 m-2 d-block">
<ApiVersionPicker />
</span>
)}
<ActionList.Group data-testid="open-xs-mobile-menu">
<LanguagePicker xs={true} />
<ActionList.Divider />
<VersionPicker xs={true} />
{signupCTAVisible && (
<>
<ActionList.Divider />
<ActionList.LinkItem
href="https://github.com/signup?ref_cta=Sign+up&ref_loc=docs+header&ref_page=docs"
target="_blank"
rel="noopener"
data-testid="xs-mobile-signup"
className="d-flex color-fg-muted"
>
{t`sign_up_cta`}
<LinkExternalIcon
className="height-full float-right"
aria-label="(external site)"
/>
</ActionList.LinkItem>
</>
)}{' '}
</ActionList.Group>
</ActionList>
<LanguagePicker mediumOrLower={true} />
{signupCTAVisible && (
<Link
href="https://github.com/signup?ref_cta=Sign+up&ref_loc=docs+header&ref_page=docs"
target="_blank"
rel="noopener"
data-testid="mobile-signup"
className="d-flex flex-justify-between flex-items-center color-fg-muted border-top px-3 py-3"
className="hide-sm d-flex flex-justify-between flex-items-center color-fg-muted border-top px-3 py-3"
>
{t`sign_up_cta`}
<LinkExternalIcon aria-label="(external site)" />
</Link>
)}
</div>
</AnchoredOverlay>
)}{' '}
</ActionMenu.Overlay>
</ActionMenu>
</div>
</div>
</div>

View File

@@ -1,16 +1,17 @@
import { useRouter } from 'next/router'
import { GlobeIcon } from '@primer/octicons-react'
import { GlobeIcon, KebabHorizontalIcon } from '@primer/octicons-react'
import { useLanguages } from 'components/context/LanguagesContext'
import { useTranslation } from 'components/hooks/useTranslation'
import { useUserLanguage } from 'components/hooks/useUserLanguage'
import { Picker } from 'src/tools/components/Picker'
import { ActionList, ActionMenu, IconButton, Link } from '@primer/react'
type Props = {
xs?: boolean
mediumOrLower?: boolean
}
export const LanguagePicker = ({ mediumOrLower }: Props) => {
export const LanguagePicker = ({ xs, mediumOrLower }: Props) => {
const router = useRouter()
const { languages } = useLanguages()
const { setUserLanguageCookie } = useUserLanguage()
@@ -37,39 +38,80 @@ export const LanguagePicker = ({ mediumOrLower }: Props) => {
// in a "denormalized" way.
const routerPath = router.asPath.split('#')[0]
return (
<div data-testid="language-picker">
<Picker
defaultText={t('language_picker_default_text')}
items={langs.map((lang) => ({
text: lang.nativeName || lang.name,
selected: lang === selectedLang,
href: `/${lang.code}${routerPath}`,
extra: {
locale: lang.code,
},
}))}
pickerLabel={mediumOrLower ? 'Language' : ''}
iconButton={mediumOrLower ? undefined : GlobeIcon}
onSelect={(item) => {
if (item.extra?.locale) {
try {
setUserLanguageCookie(item.extra.locale)
} catch (err) {
// You can never be too careful because setting a cookie
// can fail. For example, some browser
// extensions disallow all setting of cookies and attempts
// at the `document.cookie` setter could throw. Just swallow
// and move on.
console.warn('Unable to set preferred language cookie', err)
}
// languageList is specifically <ActionList.Item>'s which are reused
// for menus that behave differently at the breakpoints.
const languageList = langs.map((lang) => (
<ActionList.Item
key={`/${lang.code}${routerPath}`}
selected={lang === selectedLang}
as={Link}
href={`/${lang.code}${routerPath}`}
onSelect={() => {
if (lang.code) {
try {
setUserLanguageCookie(lang.code)
} catch (err) {
// You can never be too careful because setting a cookie
// can fail. For example, some browser
// extensions disallow all setting of cookies and attempts
// at the `document.cookie` setter could throw. Just swallow
// and move on.
console.warn('Unable to set preferred language cookie', err)
}
}}
buttonBorder={mediumOrLower}
dataTestId="default-language"
ariaLabel={`Select language: current language is ${selectedLang.name}`}
alignment={mediumOrLower ? 'start' : 'end'}
/>
}
}}
>
<span data-testid="default-language">{lang.nativeName || lang.name}</span>
</ActionList.Item>
))
// At large breakpoints, we return the full <ActionMenu> with just the languages,
// at smaller breakpoints, we return just the <ActionList> with its items so that
// the <Header> component can place it inside its own <ActionMenu> with multiple
// groups, language being just one of those groups.
return (
<div data-testid="language-picker" className="d-flex">
{xs ? (
<>
{/* XS Mobile Menu */}
<ActionMenu>
<ActionMenu.Anchor>
<ActionMenu.Button
variant="invisible"
className="color-fg-default width-full"
aria-label={`Select language: current language is ${selectedLang.name}`}
sx={{
height: 'auto',
textAlign: 'left',
'span:first-child': { display: 'inline' },
}}
>
<span style={{ whiteSpace: 'pre-wrap' }}>{t('language_picker_label') + '\n'}</span>
<span className="color-fg-muted text-normal f6">{selectedLang.name}</span>
</ActionMenu.Button>
</ActionMenu.Anchor>
<ActionMenu.Overlay align="start">
<ActionList selectionVariant="single">{languageList}</ActionList>
</ActionMenu.Overlay>
</ActionMenu>
</>
) : mediumOrLower ? (
<ActionList className="hide-sm" selectionVariant="single">
<ActionList.Group title={t('language_picker_label')}>{languageList}</ActionList.Group>
</ActionList>
) : (
<ActionMenu>
<ActionMenu.Anchor>
<IconButton
icon={mediumOrLower ? KebabHorizontalIcon : GlobeIcon}
aria-label={`Select language: current language is ${selectedLang.name}`}
/>
</ActionMenu.Anchor>
<ActionMenu.Overlay align="end">
<ActionList selectionVariant="single">{languageList}</ActionList>
</ActionMenu.Overlay>
</ActionMenu>
)}
</div>
)
}

View File

@@ -9,10 +9,10 @@ import { Picker } from 'src/tools/components/Picker'
import styles from './VersionPicker.module.scss'
type Props = {
mediumOrLower?: boolean
xs?: boolean
}
export const VersionPicker = ({ mediumOrLower }: Props) => {
export const VersionPicker = ({ xs }: Props) => {
const router = useRouter()
const { currentVersion } = useVersion()
const { allVersions, page, enterpriseServerVersions } = useMainContext()
@@ -81,14 +81,14 @@ export const VersionPicker = ({ mediumOrLower }: Props) => {
}
return (
<div data-testid="version-picker">
<div data-testid="version-picker" className={xs ? 'd-flex' : ''}>
<Picker
defaultText={t('version_picker_default_text')}
items={allLinks}
alignment="start"
pickerLabel="Version"
alignment="end"
pickerLabel={xs ? `Version\n` : `Version: `}
dataTestId="field"
buttonBorder={mediumOrLower}
descriptionFontSize={xs ? 6 : 5}
ariaLabel={`Select GitHub product version: current version is ${currentVersion}`}
renderItem={(item) => {
return (

View File

@@ -17,7 +17,7 @@ header:
sign_up_cta: Sign up
menu: Menu
picker:
language_picker_default_text: Choose a language
language_picker_label: Language
product_picker_default_text: All products
version_picker_default_text: Choose a version
release_notes:

View File

@@ -79,11 +79,11 @@ export const ApiVersionPicker = () => {
// This only shows the REST Version picker if it's calendar date versioned
return allVersions[currentVersion].apiVersions.length > 0 ? (
<div className="mb-3">
<div data-testid="api-version-picker" className="width-full">
<div data-testid="api-version-picker">
<Picker
defaultText={currentDateDisplayText}
items={apiVersionLinks}
pickerLabel="API Version"
pickerLabel="API Version: "
alignment="start"
buttonBorder={true}
dataTestId="version"

View File

@@ -1,6 +1,5 @@
import { ReactNode, useState } from 'react'
import { ActionMenu, IconButton } from '@primer/react'
import { Icon } from '@primer/octicons-react'
import { ActionMenu } from '@primer/react'
import { AnchorAlignment } from '@primer/behaviors'
@@ -8,7 +7,6 @@ import { Fields } from './Fields'
interface Props {
items: PickerItem[]
iconButton?: Icon
onSelect?: (item: PickerItem) => void
buttonBorder?: boolean
pickerLabel?: string
@@ -16,6 +14,7 @@ interface Props {
defaultText: string
ariaLabel: string
alignment: AnchorAlignment
descriptionFontSize?: number
renderItem?: (item: PickerItem) => ReactNode | string
}
@@ -31,39 +30,38 @@ export interface PickerItem {
export const Picker = ({
items,
iconButton,
ariaLabel,
pickerLabel,
buttonBorder,
dataTestId,
defaultText,
onSelect,
buttonBorder,
alignment,
descriptionFontSize,
renderItem,
}: Props) => {
const [open, setOpen] = useState(false)
const selectedOption = items.find((item) => item.selected === true)
return (
<ActionMenu open={open} onOpenChange={setOpen}>
{iconButton ? (
<ActionMenu.Anchor>
<IconButton icon={iconButton} aria-label={ariaLabel} />
</ActionMenu.Anchor>
) : (
<ActionMenu.Button
aria-label={ariaLabel}
variant={buttonBorder ? 'default' : 'invisible'}
sx={{
color: `var(--color-fg-default)`,
width: '100%',
display: 'flex',
justifyContent: 'space-between',
}}
<ActionMenu.Button
aria-label={ariaLabel}
variant={buttonBorder ? 'default' : 'invisible'}
className="color-fg-default width-full p-1 pl-2 pr-2"
sx={{
height: 'auto',
textAlign: 'left',
'span:first-child': { display: 'inline' },
}}
>
{pickerLabel && <span style={{ whiteSpace: 'pre-wrap' }}>{`${pickerLabel}`}</span>}
<span
className={`f${descriptionFontSize} color-fg-muted text-normal`}
data-testid={dataTestId}
>
{pickerLabel && <span className="color-fg-muted text-normal">{`${pickerLabel}: `}</span>}
<span data-testid={dataTestId}>{selectedOption?.text || defaultText}</span>
</ActionMenu.Button>
)}
{selectedOption?.text || defaultText}
</span>
</ActionMenu.Button>
<ActionMenu.Overlay width="auto" align={alignment}>
<Fields
open={open}

View File

@@ -17,7 +17,7 @@ header:
sign_up_cta: Sign up
menu: Menu
picker:
language_picker_default_text: Choose a language
language_picker_label: Language
product_picker_default_text: All products
version_picker_default_text: Choose a version
release_notes:

View File

@@ -332,7 +332,7 @@ test.describe('test nav at different viewports', () => {
// language picker is in mobile menu
await page.getByTestId('mobile-menu').click()
await page.getByRole('button', { name: 'Select language: current language is English' }).click()
await page.getByTestId('language-picker')
await expect(page.getByRole('menuitemradio', { name: 'English' })).toBeVisible()
// sign up button is in mobile menu
@@ -346,7 +346,41 @@ test.describe('test nav at different viewports', () => {
test('small viewports - 544-767', async ({ page }) => {
page.setViewportSize({
width: 500,
width: 555,
height: 700,
})
await page.goto('/get-started/foo/bar')
// header sign-up button is not visible
await expect(page.getByTestId('header-signup')).not.toBeVisible()
// language picker is not visible
await expect(page.getByTestId('language-picker')).not.toBeVisible()
// version picker is visible
await expect(
page.getByRole('button', {
name: 'Select GitHub product version: current version is free-pro-team@latest',
}),
).toBeVisible()
// language picker is in mobile menu
await page.getByTestId('mobile-menu').click()
await page.getByTestId('language-picker')
await expect(page.getByRole('menuitemradio', { name: 'English' })).toBeVisible()
// sign up button is in mobile menu
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.getByTestId('sidebar-product-dialog')).toBeVisible()
})
test('x-small viewports - 0-544', async ({ page }) => {
page.setViewportSize({
width: 345,
height: 700,
})
await page.goto('/get-started/foo/bar')
@@ -367,13 +401,17 @@ test.describe('test nav at different viewports', () => {
// version picker is in mobile menu
await expect(page.getByTestId('version-picker')).not.toBeVisible()
await page.getByTestId('mobile-menu').click()
await expect(page.getByTestId('open-mobile-menu').getByTestId('version-picker')).toBeVisible()
await expect(
page.getByTestId('open-xs-mobile-menu').getByTestId('version-picker'),
).toBeVisible()
// language picker is in mobile menu
await expect(page.getByTestId('open-mobile-menu').getByTestId('language-picker')).toBeVisible()
await expect(
page.getByTestId('open-xs-mobile-menu').getByTestId('language-picker'),
).toBeVisible()
// sign up button is in mobile menu
await expect(page.getByTestId('open-mobile-menu').getByTestId('version-picker')).toBeVisible()
await expect(page.getByTestId('xs-mobile-signup')).toBeVisible()
// hamburger button for sidebar overlay is visible
await expect(page.getByTestId('sidebar-hamburger')).toBeVisible()