feat: adding a skip to search anchor (#850)

Co-authored-by: Luke Karrys <luke@lukekarrys.com>

Ivan Demchenko committed Dec 4, 2023 at 23:29 UTC e388aee29541fd6a2a6af02cc29e740e20582172
4 files changed +51 -34
gatsby-browser.js
+8
@@ -1,3 +1,5 @@
1 +import {SKIP_TO_SEARCH_ID} from './src/constants'
2 +
3 export {default as wrapPageElement} from './src/page'
4 export {default as wrapRootElement} from './src/root'
5
@@ -5,3 +7,9 @@ export const shouldUpdateScroll = ({routerProps}) => {
7 const {scrollUpdate = true} = routerProps.location.state ?? {}
8 return scrollUpdate
9 }
10 +
11 +export const onRouteUpdate = ({location, prevLocation}) => {
12 + if (location.hash === `#${SKIP_TO_SEARCH_ID}` && prevLocation?.hash !== `#${SKIP_TO_SEARCH_ID}`) {
13 + document.getElementById(SKIP_TO_SEARCH_ID)?.focus()
14 + }
15 +}
src/components/skip-nav.js
+33 -32
@@ -1,49 +1,50 @@
1 import React from 'react'
2 -import {Box} from '@primer/react'
2 +import {Box, themeGet} from '@primer/react'
3 import styled from 'styled-components'
4 import Link from './link'
5 -import {SCROLL_MARGIN_TOP} from '../constants'
5 +import {SCROLL_MARGIN_TOP, SKIP_TO_CONTENT_ID} 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 -)
7 +export const SkipLink = styled(Link)`
8 + color: ${themeGet('colors.accent.emphasis')};
9 + padding: ${themeGet('space.1')};
10 + &:focus {
11 + text-decoration: underline;
12 + }
13 +`
14
15 // The following rules are to ensure that the element is visually hidden, unless
16 // it has focus. This is the recommended way to hide content from:
17 // https://webaim.org/techniques/css/invisiblecontent/#techniques
27 -export const SkipLink = styled(SkipLinkBase)`
18 +export const SkipBox = styled.div`
19 + display: inline-flex;
20 z-index: 20;
29 - width: auto;
30 - height: auto;
31 - clip: auto;
32 - position: absolute;
33 - overflow: hidden;
21 left: 10px;
22 + gap: 3px;
23 + position: absolute;
24 + transform: translateY(-100%);
25 + transition: transform 0.3s;
26 + padding: ${themeGet('space.2')};
27 + background-color: ${themeGet('colors.canvas.default')};
28 + border: 1px solid ${themeGet('colors.accent.emphasis')};
29 + border-top: 0;
30 + font-size: ${themeGet('fontSizes.1')};
31 + border-radius: 0 0 ${themeGet('radii.2')} ${themeGet('radii.2')};
32 +
33 +
34 + &:focus-within {
35 + transform: translateY(0%);
36 + }
37 +
38 + & > * {
39 + margin-right: ${themeGet('space.1')};
40 + }
41
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;
42 + & > *:last-child {
43 + margin-right: 0;
44 }
45 `
46
46 -const SkipNavBase = props => <Box id={ID} {...props} />
47 +const SkipNavBase = props => <Box id={SKIP_TO_CONTENT_ID} {...props} />
48
49 export const SkipNav = styled(SkipNavBase)`
50 scroll-margin-top: ${SCROLL_MARGIN_TOP}px;
src/constants.js
+4
@@ -6,4 +6,8 @@ export const FULL_HEADER_HEIGHT = HEADER_HEIGHT + HEADER_BAR
6
7 export const SCROLL_MARGIN_TOP = FULL_HEADER_HEIGHT + 24
8
9 +export const SKIP_TO_CONTENT_ID = 'skip-to-content'
10 +
11 +export const SKIP_TO_SEARCH_ID = 'search-box-input'
12 +
13 export const CLI_PATH = '/cli'
src/page.js
+6 -2
@@ -4,7 +4,8 @@ import {createGlobalStyle} from 'styled-components'
4 import Slugger from 'github-slugger'
5 import Header from './components/header'
6 import Sidebar from './components/sidebar'
7 -import {SkipLink} from './components/skip-nav'
7 +import {SkipBox, SkipLink} from './components/skip-nav'
8 +import {SKIP_TO_CONTENT_ID, SKIP_TO_SEARCH_ID} from './constants'
9
10 import {PageProvider} from './hooks/use-page'
11 import Layout from './layout'
@@ -27,7 +28,10 @@ const PageElement = ({element, props}) => {
28 return (
29 <BaseStyles>
30 <GlobalStyles />
30 - <SkipLink />
31 + <SkipBox>
32 + <SkipLink href={`#${SKIP_TO_SEARCH_ID}`}>Skip to search</SkipLink>
33 + <SkipLink href={`#${SKIP_TO_CONTENT_ID}`}>Skip to content</SkipLink>
34 + </SkipBox>
35 <PageProvider value={page}>
36 <Box sx={{display: 'flex', flexDirection: 'column', minHeight: '100vh'}}>
37 <Header />