Skip to content

Commit fd531e3

Browse files
committed
Return focus to variant after cross variant navigation
Closes #483
1 parent 34ebcf3 commit fd531e3

File tree

12 files changed

+210
-149
lines changed

12 files changed

+210
-149
lines changed

cli/lib/build.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const updateNav = async (updates, {nav, path}) => {
1414
shortName: release.id,
1515
url: release.url,
1616
default: release.default,
17+
type: release.type,
1718
children: release.nav,
1819
}))
1920

@@ -43,7 +44,7 @@ const getCurrentVersions = nav => {
4344

4445
const currentVersions = currentSections
4546
.map(v => {
46-
const version = v.title?.match(/^Version\s(.*?)\s/)[1]
47+
const version = v.title?.match(/^Version\s(.*?)$/)[1]
4748
return version
4849
})
4950
.sort(semver.compare)
@@ -95,16 +96,17 @@ const main = async ({loglevel, releases: rawReleases, useCurrent, navPath, conte
9596

9697
const releases = releaseVersions.map(release => {
9798
const type = release.default
98-
? 'Latest Release'
99+
? 'latest'
99100
: release.prerelease
100-
? 'Prerelease'
101+
? 'prerelease'
101102
: semver.gt(release.version, latestRelease.version)
102-
? 'Current Release'
103-
: 'Legacy Release'
103+
? 'current'
104+
: 'legacy'
104105

105106
return {
106107
...release,
107-
title: `Version ${release.version} (${type})`,
108+
type,
109+
title: `Version ${release.version}`,
108110
url: `/${DOCS_PATH}/${release.id}`,
109111
urlPrefix: DOCS_PATH,
110112
urlPrefixes: [DOCS_PATH, `${DOCS_PATH}-documentation`],

content/nav.yml

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -283,10 +283,11 @@
283283
shortName: CLI
284284
url: /cli
285285
variants:
286-
- title: Version 6.14.18 (Legacy Release)
286+
- title: Version 6.14.18
287287
shortName: v6
288288
url: /cli/v6
289289
default: false
290+
type: legacy
290291
children:
291292
- title: CLI Commands
292293
shortName: Commands
@@ -525,10 +526,11 @@
525526
- title: Changelog
526527
url: /cli/v6/using-npm/changelog
527528
description: Changelog notes for each version
528-
- title: Version 7.24.2 (Legacy Release)
529+
- title: Version 7.24.2
529530
shortName: v7
530531
url: /cli/v7
531532
default: false
533+
type: legacy
532534
children:
533535
- title: CLI Commands
534536
shortName: Commands
@@ -782,10 +784,11 @@
782784
- title: Changelog
783785
url: /cli/v7/using-npm/changelog
784786
description: Changelog notes for each version
785-
- title: Version 8.19.4 (Legacy Release)
787+
- title: Version 8.19.4
786788
shortName: v8
787789
url: /cli/v8
788790
default: false
791+
type: legacy
789792
children:
790793
- title: CLI Commands
791794
shortName: Commands
@@ -1051,10 +1054,11 @@
10511054
- title: Changelog
10521055
url: /cli/v8/using-npm/changelog
10531056
description: Changelog notes for each version
1054-
- title: Version 9.9.0 (Legacy Release)
1057+
- title: Version 9.9.0
10551058
shortName: v9
10561059
url: /cli/v9
10571060
default: false
1061+
type: legacy
10581062
children:
10591063
- title: CLI Commands
10601064
shortName: Commands
@@ -1320,10 +1324,11 @@
13201324
- title: Changelog
13211325
url: /cli/v9/using-npm/changelog
13221326
description: Changelog notes for each version
1323-
- title: Version 10.2.1 (Latest Release)
1327+
- title: Version 10.2.1
13241328
shortName: v10
13251329
url: /cli/v10
13261330
default: true
1331+
type: latest
13271332
children:
13281333
- title: CLI Commands
13291334
shortName: Commands

src/components/skip-nav.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React from 'react'
2+
import {Box} from '@primer/react'
3+
import styled from 'styled-components'
4+
import Link from './link'
5+
import {SCROLL_MARGIN_TOP} from '../constants'
6+
7+
const ID = 'skip-nav'
8+
9+
const SkipLinkBase = props => (
10+
<Link
11+
{...props}
12+
href={`#${ID}`}
13+
sx={{
14+
p: 3,
15+
color: 'fg.onEmphasis',
16+
backgroundColor: 'accent.emphasis',
17+
fontSize: 1,
18+
}}
19+
>
20+
Skip to content
21+
</Link>
22+
)
23+
24+
// The following rules are to ensure that the element is visually hidden, unless
25+
// it has focus. This is the recommended way to hide content from:
26+
// https://webaim.org/techniques/css/invisiblecontent/#techniques
27+
export const SkipLink = styled(SkipLinkBase)`
28+
z-index: 20;
29+
width: auto;
30+
height: auto;
31+
clip: auto;
32+
position: absolute;
33+
overflow: hidden;
34+
left: 10px;
35+
36+
&:not(:focus) {
37+
clip: rect(1px, 1px, 1px, 1px);
38+
clip-path: inset(50%);
39+
height: 1px;
40+
width: 1px;
41+
margin: -1px;
42+
padding: 0;
43+
}
44+
`
45+
46+
const SkipNavBase = props => <Box id={ID} {...props} />
47+
48+
export const SkipNav = styled(SkipNavBase)`
49+
scroll-margin-top: ${SCROLL_MARGIN_TOP}px;
50+
`

src/components/table-of-contents.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from 'react'
22
import {Heading, Box, Details, useDetails, Button} from '@primer/react'
33
import {ChevronDownIcon, ChevronRightIcon} from '@primer/octicons-react'
44
import {NavList} from '@primer/react/drafts'
5-
import {FULL_HEADER_HEIGHT} from '../constants'
5+
import {SCROLL_MARGIN_TOP} from '../constants'
66
import usePage from '../hooks/use-page'
77

88
const TableOfContentsItems = ({items, depth}) => (
@@ -54,16 +54,17 @@ export const Desktop = withTableOfContents(({items}) => (
5454
marginLeft: [null, 7, 8, 9],
5555
display: ['none', null, 'block'],
5656
position: 'sticky',
57-
top: FULL_HEADER_HEIGHT + 48,
58-
maxHeight: `calc(100vh - ${FULL_HEADER_HEIGHT + 48}px)`,
57+
top: SCROLL_MARGIN_TOP,
58+
maxHeight: `calc(100vh - ${SCROLL_MARGIN_TOP}px)`,
5959
}}
6060
>
6161
<Heading as="h3" sx={{fontSize: 1, display: 'inline-block', fontWeight: 'bold'}} id="toc-heading">
6262
Table of contents
6363
</Heading>
6464
<Box
6565
sx={{
66-
maxHeight: `calc(100% - 21px)`,
66+
// extra pixels to account for table of contents title height
67+
maxHeight: `calc(100% - 24px)`,
6768
overflowY: 'scroll',
6869
}}
6970
>

src/components/variant-select.js

Lines changed: 99 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,131 @@
11
import React from 'react'
22
import {ActionList, ActionMenu, Box} from '@primer/react'
3-
import {navigate} from 'gatsby'
43
import * as getNav from '../util/get-nav'
54
import usePage from '../hooks/use-page'
5+
import Link from './link'
66

7-
const VariantItem = ({match, active}) => {
8-
const {variant, page} = match
7+
const VariantItem = ({title, shortName, url, active}) => (
8+
<ActionList.Item
9+
as={Link}
10+
to={url}
11+
id={shortName}
12+
active={active}
13+
sx={{
14+
':hover': {textDecoration: 'none'},
15+
}}
16+
>
17+
{title}
18+
</ActionList.Item>
19+
)
920

10-
const navigateToPage = React.useCallback(() => navigate(`${page.url}?v=true`), [page.url])
21+
const useVariantFocus = path => {
22+
const anchorRef = React.useRef(null)
23+
const pathRef = React.useRef(null)
1124

12-
const handleClick = React.useCallback(
13-
event => {
14-
event.preventDefault()
15-
navigateToPage()
16-
},
17-
[navigateToPage],
18-
)
25+
React.useEffect(() => {
26+
const previousPath = pathRef.current
27+
pathRef.current = path
1928

20-
const handleKey = React.useCallback(
21-
event => {
22-
if (event.key === 'Enter') {
23-
navigateToPage()
29+
if (getNav.didVariantChange(previousPath, path)) {
30+
const anchor = anchorRef.current
31+
const onBlur = () => {
32+
anchor.removeEventListener('blur', onBlur)
33+
anchor.focus()
2434
}
25-
},
26-
[navigateToPage],
27-
)
35+
anchor.addEventListener('blur', onBlur)
36+
return () => anchor.removeEventListener('blur', onBlur)
37+
}
38+
}, [path])
2839

29-
return (
30-
<ActionList.Item onKeyDown={handleKey} onClick={handleClick} id={variant.shortName} active={active}>
31-
{variant.title}
32-
</ActionList.Item>
33-
)
40+
return anchorRef
3441
}
3542

36-
const VariantMenu = ({variants, path}) => {
43+
const VariantMenu = ({title, latest, current, prerelease, legacy, path}) => {
3744
const [open, setOpen] = React.useState(false)
38-
39-
const {selected, items} = variants.reduce(
40-
(acc, match, key) => {
41-
const active = match.page.url === path
42-
if (active) {
43-
acc.selected = match
44-
}
45-
acc.items.push({match, key, active})
46-
return acc
47-
},
48-
{selected: variants[0], items: []},
49-
)
45+
const anchorRef = useVariantFocus(path)
46+
const labelId = 'label-versions-list-item'
5047

5148
return (
5249
<>
53-
<Box as="p" sx={{m: 0}} id="label-versions-list-item">
50+
<Box as="p" sx={{m: 0}} id={labelId}>
5451
Select CLI Version:
5552
</Box>
56-
<ActionMenu open={open} onOpenChange={setOpen}>
57-
{/* Disabling to remove lint warnings. This property was added as "autofocus"
58-
in a previous accessibility audit which did not trigger the lint warning. */
59-
/* eslint-disable-next-line jsx-a11y/no-autofocus */}
60-
<ActionMenu.Button autoFocus aria-describedby="label-versions-list-item">
61-
{selected.variant.title}
62-
</ActionMenu.Button>
63-
<ActionMenu.Overlay width="medium" onEscape={() => setOpen(false)}>
64-
<ActionList id="versions-list-item" aria-labelledby="label-versions-list-item">
65-
{items.map(item => (
66-
<VariantItem key={item.key} {...item} />
67-
))}
53+
<ActionMenu anchorRef={anchorRef} open={open} onOpenChange={setOpen}>
54+
<ActionMenu.Button aria-describedby={labelId}>{title}</ActionMenu.Button>
55+
<ActionMenu.Overlay width="auto" onEscape={() => setOpen(false)}>
56+
<ActionList aria-labelledby={labelId}>
57+
<ActionList.Group title="Current">
58+
<VariantItem {...latest} />
59+
{current && <VariantItem {...current} />}
60+
{prerelease && <VariantItem {...prerelease} />}
61+
</ActionList.Group>
62+
{legacy && (
63+
<ActionList.Group title="Legacy">
64+
{legacy.map(item => (
65+
<VariantItem key={item.title} {...item} />
66+
))}
67+
</ActionList.Group>
68+
)}
6869
</ActionList>
6970
</ActionMenu.Overlay>
7071
</ActionMenu>
7172
</>
7273
)
7374
}
7475

75-
const VariantSelect = () => {
76-
const {location} = usePage()
77-
const root = getNav.getVariantRoot(location.pathname)
78-
const path = getNav.getPath(location.pathname)
79-
const vp = getNav.getVariantAndPage(root, path)
80-
const variants = vp ? getNav.getVariantsForPage(root, vp.page) : []
76+
const useVariants = () => {
77+
const {pathname} = usePage().location
8178

82-
if (!variants.length) {
83-
return null
84-
}
79+
return React.useMemo(() => {
80+
const root = getNav.getVariantRoot(pathname)
81+
const path = getNav.getPath(pathname)
82+
const vp = getNav.getVariantAndPage(root, path)
83+
const variantPages = vp ? getNav.getVariantsForPage(root, vp.page) : []
8584

86-
return (
85+
if (!variantPages.length) {
86+
return null
87+
}
88+
89+
const result = {path, latest: null, current: null, prerelease: null, legacy: []}
90+
91+
for (const {variant, page} of variantPages) {
92+
const item = {...variant, url: page.url, active: page.url === path}
93+
let typeDesc = ''
94+
switch (variant.type) {
95+
case 'latest':
96+
result.latest = item
97+
typeDesc = ' (Latest)'
98+
break
99+
case 'current':
100+
result.current = item
101+
typeDesc = ' (Current)'
102+
break
103+
case 'prerelease':
104+
result.prerelease = item
105+
typeDesc = ' (Prerelease)'
106+
break
107+
default:
108+
result.legacy.push(item)
109+
typeDesc = ' Legacy'
110+
}
111+
if (item.active) {
112+
result.title = `${item.title}${typeDesc}`
113+
}
114+
}
115+
116+
result.legacy.sort((a, b) => parseInt(b.shortName.slice(1)) - parseInt(a.shortName.slice(1)))
117+
118+
return result
119+
}, [pathname])
120+
}
121+
122+
const VariantSelect = () => {
123+
const variants = useVariants()
124+
return variants ? (
87125
<Box sx={{mt: 2, mb: 3}}>
88-
<VariantMenu variants={variants} path={path} />
126+
<VariantMenu {...variants} />
89127
</Box>
90-
)
128+
) : null
91129
}
92130

93131
export default VariantSelect

src/constants.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ export const HEADER_BAR = 10
44

55
export const FULL_HEADER_HEIGHT = HEADER_HEIGHT + HEADER_BAR
66

7-
export const SKIP_NAV = {id: 'skip-nav', as: 'main'}
7+
export const SCROLL_MARGIN_TOP = FULL_HEADER_HEIGHT + 24

0 commit comments

Comments
 (0)