Skip to content

Commit 329a8c6

Browse files
authored
Create a shared picker component (#22836)
* Unified picker component * Add picker to storybook * TS fixes * Simplify "mobile" header spacing * Fix a few testid * Update Picker.tsx * Update Picker.tsx * Update Picker.tsx * Fix unit test * Fix rendering tests
1 parent 07a5edd commit 329a8c6

10 files changed

Lines changed: 212 additions & 239 deletions

File tree

components/page-header/Header.tsx

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export const Header = () => {
6060
</div>
6161

6262
<div className="mr-2">
63-
<VersionPicker variant="compact" />
63+
<VersionPicker />
6464
</div>
6565

6666
<LanguagePicker />
@@ -106,25 +106,21 @@ export const Header = () => {
106106
<div
107107
className={cx('width-full position-sticky top-0', isMenuOpen ? 'd-block' : 'd-none')}
108108
>
109-
<div className="mt-3 mb-2">
110-
<div className="pt-3 mb-4 ml-2">
111-
<Breadcrumbs />
112-
</div>
113-
<h4 className="f5 text-normal color-fg-muted ml-3">{t('explore_by_product')}</h4>
114-
115-
<ProductPicker />
109+
<div className="my-4">
110+
<Breadcrumbs />
116111
</div>
117112

118-
<div className="border-top my-2 mx-3" />
113+
<ProductPicker />
114+
115+
<div className="border-top my-2" />
119116
<VersionPicker variant="inline" />
120117

121-
{/* <!-- Language picker - 'English', 'Japanese', etc --> */}
122-
<div className="border-top my-2 mx-3" />
118+
<div className="border-top my-2" />
123119
<LanguagePicker variant="inline" />
124120

125121
{/* <!-- GitHub.com homepage and 404 page has a stylized search; Enterprise homepages do not --> */}
126122
{relativePath !== 'index.md' && error !== '404' && (
127-
<div className="pt-3 mx-3">
123+
<div className="my-2 pt-3">
128124
<Search iconSize={16} isMobileSearch={true} />
129125
</div>
130126
)}
Lines changed: 24 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,41 @@
11
import { useRouter } from 'next/router'
2-
import { Box, Dropdown, Details, Text, useDetails } from '@primer/components'
3-
import { ChevronDownIcon } from '@primer/octicons-react'
4-
52
import { Link } from 'components/Link'
63
import { useLanguages } from 'components/context/LanguagesContext'
4+
import { Picker } from 'components/ui/Picker'
75

86
type Props = {
97
variant?: 'inline'
108
}
9+
1110
export const LanguagePicker = ({ variant }: Props) => {
1211
const router = useRouter()
1312
const { languages } = useLanguages()
14-
const { getDetailsProps, setOpen } = useDetails({ closeOnOutsideClick: true })
1513
const locale = router.locale || 'en'
1614
const langs = Object.values(languages)
1715
const selectedLang = languages[locale]
1816

19-
if (variant === 'inline') {
20-
return (
21-
<Details {...getDetailsProps()} data-testid="language-picker">
22-
<summary
23-
className="d-block btn btn-invisible color-fg-default"
24-
aria-label="Toggle language list"
25-
>
26-
<div className="d-flex flex-items-center flex-justify-between">
27-
<Text>{selectedLang.nativeName || selectedLang.name}</Text>
28-
<ChevronDownIcon size={24} className="arrow ml-md-1" />
29-
</div>
30-
</summary>
31-
<Box mt={1}>
32-
{langs.map((lang) => {
33-
if (lang.wip) {
34-
return null
35-
}
36-
37-
return (
38-
<Dropdown.Item onClick={() => setOpen(false)} key={lang.code}>
39-
<Link href={router.asPath} locale={lang.code}>
40-
{lang.nativeName ? (
41-
<>
42-
{lang.nativeName} ({lang.name})
43-
</>
44-
) : (
45-
lang.name
46-
)}
47-
</Link>
48-
</Dropdown.Item>
49-
)
50-
})}
51-
</Box>
52-
</Details>
53-
)
54-
}
55-
5617
return (
57-
<Details {...getDetailsProps()} data-testid="language-picker" className="position-relative">
58-
<summary className="d-block btn btn-invisible color-fg-default">
59-
<Text>{selectedLang.nativeName || selectedLang.name}</Text>
60-
<Dropdown.Caret />
61-
</summary>
62-
<Dropdown.Menu direction="sw" style={{ width: 'unset' }}>
63-
{langs.map((lang) => {
64-
if (lang.wip) {
65-
return null
66-
}
67-
68-
return (
69-
<Dropdown.Item key={lang.code} onClick={() => setOpen(false)}>
70-
<Link href={router.asPath} locale={lang.code}>
71-
{lang.nativeName ? (
72-
<>
73-
{lang.nativeName} ({lang.name})
74-
</>
75-
) : (
76-
lang.name
77-
)}
78-
</Link>
79-
</Dropdown.Item>
80-
)
81-
})}
82-
</Dropdown.Menu>
83-
</Details>
18+
<Picker
19+
variant={variant}
20+
data-testid="language-picker"
21+
defaultText="Choose language"
22+
options={langs
23+
.filter((lang) => !lang.wip)
24+
.map((lang) => ({
25+
text: lang.nativeName || lang.name,
26+
selected: lang === selectedLang,
27+
item: (
28+
<Link href={router.asPath} locale={lang.code}>
29+
{lang.nativeName ? (
30+
<>
31+
{lang.nativeName} ({lang.name})
32+
</>
33+
) : (
34+
lang.name
35+
)}
36+
</Link>
37+
),
38+
}))}
39+
/>
8440
)
8541
}

components/page-header/ProductPicker.tsx

Lines changed: 22 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,47 +2,33 @@ import { useRouter } from 'next/router'
22

33
import { Link } from 'components/Link'
44
import { useMainContext } from 'components/context/MainContext'
5-
import { ChevronDownIcon, LinkExternalIcon } from '@primer/octicons-react'
6-
import { Box, Dropdown, Details, useDetails } from '@primer/components'
5+
import { LinkExternalIcon } from '@primer/octicons-react'
6+
import { Picker } from 'components/ui/Picker'
77

8-
// Product Picker - GitHub.com, Enterprise Server, etc
98
export const ProductPicker = () => {
109
const router = useRouter()
1110
const { activeProducts, currentProduct } = useMainContext()
12-
const { getDetailsProps, setOpen } = useDetails({ closeOnOutsideClick: true })
1311

1412
return (
15-
<Details {...getDetailsProps()} className="details-reset">
16-
<summary
17-
className="d-block color-fg-default btn btn-invisible"
18-
role="button"
19-
aria-label="Toggle products list"
20-
>
21-
<div
22-
data-testid="current-product"
23-
data-current-product-path={currentProduct?.href}
24-
className="d-flex flex-items-center flex-justify-between"
25-
>
26-
<span>{currentProduct?.name || 'All Products'}</span>
27-
<ChevronDownIcon size={24} className="arrow ml-md-1" />
28-
</div>
29-
</summary>
30-
<Box data-testid="product-picker-list" py="2" style={{ zIndex: 6 }}>
31-
{activeProducts.map((product) => {
32-
return (
33-
<Dropdown.Item key={product.id} onClick={() => setOpen(false)}>
34-
<Link href={`${product.external ? '' : `/${router.locale}`}${product.href}`}>
35-
{product.name}
36-
{product.external && (
37-
<span className="ml-1">
38-
<LinkExternalIcon size="small" />
39-
</span>
40-
)}
41-
</Link>
42-
</Dropdown.Item>
43-
)
44-
})}
45-
</Box>
46-
</Details>
13+
<Picker
14+
variant="inline"
15+
data-testid="product-picker"
16+
data-current-product-path={currentProduct?.href}
17+
defaultText="All products"
18+
options={activeProducts.map((product) => ({
19+
text: product.name,
20+
selected: product === currentProduct,
21+
item: (
22+
<Link href={`${product.external ? '' : `/${router.locale}`}${product.href}`}>
23+
{product.name}
24+
{product.external && (
25+
<span className="ml-1">
26+
<LinkExternalIcon size="small" />
27+
</span>
28+
)}
29+
</Link>
30+
),
31+
}))}
32+
/>
4733
)
4834
}
Lines changed: 29 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,52 @@
11
import { useRouter } from 'next/router'
2-
import cx from 'classnames'
3-
import { Dropdown, Details, Box, Text, useDetails } from '@primer/components'
4-
import { ArrowRightIcon, ChevronDownIcon } from '@primer/octicons-react'
2+
import { ArrowRightIcon } from '@primer/octicons-react'
53

64
import { Link } from 'components/Link'
75
import { useMainContext } from 'components/context/MainContext'
86
import { useVersion } from 'components/hooks/useVersion'
97
import { useTranslation } from 'components/hooks/useTranslation'
8+
import { Picker } from 'components/ui/Picker'
109

1110
type Props = {
12-
variant?: 'inline' | 'compact'
11+
variant?: 'inline'
1312
}
13+
1414
export const VersionPicker = ({ variant }: Props) => {
1515
const router = useRouter()
1616
const { currentVersion } = useVersion()
1717
const { allVersions, page, enterpriseServerVersions } = useMainContext()
18-
const { getDetailsProps, setOpen } = useDetails({ closeOnOutsideClick: true })
1918
const { t } = useTranslation('pages')
2019

2120
if (page.permalinks && page.permalinks.length <= 1) {
2221
return null
2322
}
2423

2524
return (
26-
<>
27-
<div>
28-
<Details
29-
{...getDetailsProps()}
30-
className={cx(
31-
'position-relative details-reset',
32-
variant === 'inline' ? 'd-block' : 'd-inline-block'
33-
)}
34-
data-testid="article-version-picker"
35-
>
36-
<summary
37-
className="d-block btn btn-invisible color-fg-default"
38-
aria-haspopup="true"
39-
aria-label="Toggle version list"
40-
>
41-
{variant === 'inline' ? (
42-
<div className="d-flex flex-items-center flex-justify-between">
43-
<Text>{allVersions[currentVersion].versionTitle}</Text>
44-
<ChevronDownIcon size={24} className="arrow ml-md-1" />
45-
</div>
46-
) : (
47-
<>
48-
<Text>{allVersions[currentVersion].versionTitle}</Text>
49-
<Dropdown.Caret />
50-
</>
51-
)}
52-
</summary>
53-
{variant === 'inline' ? (
54-
<Box py="2">
55-
{(page.permalinks || []).map((permalink) => {
56-
return (
57-
<Dropdown.Item key={permalink.href} onClick={() => setOpen(false)}>
58-
<Link href={permalink.href}>{permalink.pageVersionTitle}</Link>
59-
</Dropdown.Item>
60-
)
61-
})}
62-
<Box mt={1}>
63-
<Link
64-
onClick={() => {
65-
setOpen(false)
66-
}}
67-
href={`/${router.locale}/${enterpriseServerVersions[0]}/admin/all-releases`}
68-
className="f6 no-underline color-fg-muted pl-3 pr-2 no-wrap"
69-
>
70-
{t('all_enterprise_releases')}{' '}
71-
<ArrowRightIcon verticalAlign="middle" size={15} className="mr-2" />
72-
</Link>
73-
</Box>
74-
</Box>
75-
) : (
76-
<Dropdown.Menu direction="sw" style={{ width: 'unset' }}>
77-
{(page.permalinks || []).map((permalink) => {
78-
return (
79-
<Dropdown.Item key={permalink.href} onClick={() => setOpen(false)}>
80-
<Link href={permalink.href}>{permalink.pageVersionTitle}</Link>
81-
</Dropdown.Item>
82-
)
83-
})}
84-
<Box
85-
borderColor="border.default"
86-
borderTopWidth={1}
87-
borderTopStyle="solid"
88-
mt={2}
89-
pt={2}
90-
pb={1}
25+
<Picker
26+
variant={variant}
27+
data-testid="version-picker"
28+
defaultText="Choose version"
29+
options={(page.permalinks || [])
30+
.map((permalink) => ({
31+
text: permalink.pageVersionTitle,
32+
selected: allVersions[currentVersion].versionTitle === permalink.pageVersionTitle,
33+
item: <Link href={permalink.href}>{permalink.pageVersionTitle}</Link>,
34+
}))
35+
.concat([
36+
{
37+
text: t('all_enterprise_releases'),
38+
selected: false,
39+
item: (
40+
<Link
41+
href={`/${router.locale}/${enterpriseServerVersions[0]}/admin/all-releases`}
42+
className="f6 no-underline color-fg-muted"
9143
>
92-
<Link
93-
onClick={() => {
94-
setOpen(false)
95-
}}
96-
href={`/${router.locale}/${enterpriseServerVersions[0]}/admin/all-releases`}
97-
className="f6 no-underline color-fg-muted pl-3 pr-2 no-wrap"
98-
>
99-
{t('all_enterprise_releases')}{' '}
100-
<ArrowRightIcon verticalAlign="middle" size={15} className="mr-2" />
101-
</Link>
102-
</Box>
103-
</Dropdown.Menu>
104-
)}
105-
</Details>
106-
</div>
107-
</>
44+
{t('all_enterprise_releases')}{' '}
45+
<ArrowRightIcon verticalAlign="middle" size={15} className="mr-2" />
46+
</Link>
47+
),
48+
},
49+
])}
50+
/>
10851
)
10952
}

0 commit comments

Comments
 (0)