fix: cleanup hooks to use useBreakpoint for screen width changes
Luke Karrys committed
Sep 25, 2023 at 18:03 UTC
9b1c5db17a41c1040c7cb6baa2841021447971f0
4 files changed
+78
-92
theme/src/components/header.js
+4
-18
@@ -1,17 +1,14 @@
1
import {Box, Flex, Link, Sticky} from '@primer/components'
2
-import {ThreeBarsIcon} from '@primer/octicons-react'
2
import {Link as GatsbyLink} from 'gatsby'
3
import React from 'react'
5
-import styled, {ThemeContext} from 'styled-components'
4
+import styled from 'styled-components'
5
import headerNavItems from '../header-nav.yml'
6
import useSiteMetadata from '../use-site-metadata'
8
-import DarkButton from './dark-button'
7
import MobileSearch from './mobile-search'
10
-import NavDrawer, {useNavDrawerState} from './nav-drawer'
8
+import NavDrawer from './nav-drawer'
9
import NavDropdown, {NavDropdownItem} from './nav-dropdown'
10
import Search from './search'
11
import NpmLogo from './npm-logo'
14
-import useBreakpoint from '../use-breakpoint'
12
import useSearch from '../use-search'
13
14
export const HEADER_HEIGHT = 66
@@ -22,11 +19,8 @@ const NpmHeaderBar = styled(Box)`
19
`
20
21
function Header({location, isSearchEnabled = true}) {
25
- const theme = React.useContext(ThemeContext)
26
- const [isNavDrawerOpen, setIsNavDrawerOpen] = useNavDrawerState(theme.breakpoints[2])
22
const siteMetadata = useSiteMetadata()
28
- const isMobile = useBreakpoint(theme.breakpoints[2], 'max')
29
- const search = useSearch({isMobile})
23
+ const search = useSearch()
24
25
const logoStyle = {color: '#cb0000', marginRight: '16px'}
26
const titleStyle = {color: '#dddddd', fontWeight: '600', display: 'flex', alignItems: 'center'}
@@ -58,15 +52,7 @@ function Header({location, isSearchEnabled = true}) {
52
</Box>
53
<Flex display={['flex', null, null, 'none']}>
54
{isSearchEnabled ? <MobileSearch {...search} /> : null}
61
- <DarkButton
62
- aria-label="Menu"
63
- aria-expanded={isNavDrawerOpen}
64
- onClick={() => setIsNavDrawerOpen(true)}
65
- ml={3}
66
- >
67
- <ThreeBarsIcon />
68
- </DarkButton>
69
- <NavDrawer location={location} isOpen={isNavDrawerOpen} onDismiss={() => setIsNavDrawerOpen(false)} />
55
+ <NavDrawer location={location} />
56
</Flex>
57
</Flex>
58
</Flex>
theme/src/components/nav-drawer.js
+47
-56
@@ -1,7 +1,6 @@
1
import {BorderBox, Flex, Link, Text} from '@primer/components'
2
-import {ChevronDownIcon, ChevronUpIcon, XIcon} from '@primer/octicons-react'
2
+import {ChevronDownIcon, ChevronUpIcon, XIcon, ThreeBarsIcon} from '@primer/octicons-react'
3
import {Link as GatsbyLink} from 'gatsby'
4
-import debounce from 'lodash.debounce'
4
import React from 'react'
5
import navItems from '../nav.yml'
6
import headerNavItems from '../header-nav.yml'
@@ -10,72 +9,64 @@ import DarkButton from './dark-button'
9
import Details from './details'
10
import Drawer from './drawer'
11
import NavItems from './nav-items'
12
+import {useIsMobile} from '../use-breakpoint'
13
14
-export function useNavDrawerState(breakpoint) {
15
- // Handle string values from themes with units at the end
16
- if (typeof breakpoint === 'string') {
17
- breakpoint = parseInt(breakpoint, 10)
18
- }
19
- const [isOpen, setOpen] = React.useState(false)
20
-
21
- const debouncedOnResize = React.useMemo(
22
- () =>
23
- debounce(() => {
24
- if (window.innerWidth >= breakpoint) {
25
- setOpen(false)
26
- }
27
- }, 250),
28
- [breakpoint],
29
- )
14
+const useDrawerIsOpen = () => {
15
+ const isMobile = useIsMobile()
16
+ const [isOpen, setIsOpen] = React.useState(false)
17
+ const setOpen = React.useCallback(() => setIsOpen(true), [])
18
+ const setClose = React.useCallback(() => setIsOpen(false), [])
19
20
React.useEffect(() => {
32
- if (isOpen) {
33
- window.addEventListener('resize', debouncedOnResize)
34
- return () => {
35
- // cancel any debounced invocation of the resize handler
36
- debouncedOnResize.cancel()
37
- window.removeEventListener('resize', debouncedOnResize)
38
- }
21
+ if (!isMobile && isOpen) {
22
+ setIsOpen(false)
23
}
40
- }, [isOpen, debouncedOnResize])
24
+ }, [isMobile, isOpen])
25
42
- return [isOpen, setOpen]
26
+ return [isOpen, {setOpen, setClose}]
27
}
28
45
-function NavDrawer({location, isOpen, onDismiss}) {
29
+function NavDrawer({location}) {
30
const siteMetadata = useSiteMetadata()
31
+ const [isOpen, {setOpen, setClose}] = useDrawerIsOpen()
32
+
33
return (
48
- <Drawer isOpen={isOpen} onDismiss={onDismiss}>
49
- <Flex
50
- flexDirection="column"
51
- height="100%"
52
- bg="gray.0"
53
- style={{overflow: 'auto', WebkitOverflowScrolling: 'touch'}}
54
- >
55
- <Flex flexDirection="column" flex="1 0 auto" color="gray.7" bg="gray.0">
56
- <BorderBox borderWidth={0} borderRadius={0} borderBottomWidth={1} borderColor="gray.7">
57
- <Flex py={3} pl={4} pr={3} alignItems="center" justifyContent="space-between" color="gray.1" bg="gray.9">
58
- <Link as={GatsbyLink} to="/" display="inline-block" color="inherit">
59
- {siteMetadata.title}
60
- </Link>
61
- <DarkButton aria-label="Close" onClick={onDismiss}>
62
- <XIcon />
63
- </DarkButton>
64
- </Flex>
65
- </BorderBox>
66
- {navItems.length > 0 ? (
67
- <Flex flexDirection="column">
68
- <NavItems location={location} items={navItems} editOnGitHub={false} />
34
+ <>
35
+ <DarkButton aria-label="Menu" aria-expanded={isOpen} onClick={setOpen} ml={3}>
36
+ <ThreeBarsIcon />
37
+ </DarkButton>
38
+ <Drawer isOpen={isOpen} onDismiss={setClose}>
39
+ <Flex
40
+ flexDirection="column"
41
+ height="100%"
42
+ bg="gray.0"
43
+ style={{overflow: 'auto', WebkitOverflowScrolling: 'touch'}}
44
+ >
45
+ <Flex flexDirection="column" flex="1 0 auto" color="gray.7" bg="gray.0">
46
+ <BorderBox borderWidth={0} borderRadius={0} borderBottomWidth={1} borderColor="gray.7">
47
+ <Flex py={3} pl={4} pr={3} alignItems="center" justifyContent="space-between" color="gray.1" bg="gray.9">
48
+ <Link as={GatsbyLink} to="/" display="inline-block" color="inherit">
49
+ {siteMetadata.title}
50
+ </Link>
51
+ <DarkButton aria-label="Close" onClick={setClose}>
52
+ <XIcon />
53
+ </DarkButton>
54
+ </Flex>
55
+ </BorderBox>
56
+ {navItems.length > 0 ? (
57
+ <Flex flexDirection="column">
58
+ <NavItems location={location} items={navItems} editOnGitHub={false} />
59
+ </Flex>
60
+ ) : null}
61
+ </Flex>
62
+ {headerNavItems.length > 0 ? (
63
+ <Flex flexDirection="column" flex="1 0 auto" color="gray.1" bg="gray.9">
64
+ <HeaderNavItems items={headerNavItems} />
65
</Flex>
66
) : null}
67
</Flex>
72
- {headerNavItems.length > 0 ? (
73
- <Flex flexDirection="column" flex="1 0 auto" color="gray.1" bg="gray.9">
74
- <HeaderNavItems items={headerNavItems} />
75
- </Flex>
76
- ) : null}
77
- </Flex>
78
- </Drawer>
68
+ </Drawer>
69
+ </>
70
)
71
}
72
theme/src/use-breakpoint.js
+23
-17
@@ -1,29 +1,35 @@
1
import React from 'react'
2
+import {ThemeContext} from 'styled-components'
3
3
-function useBreakpoint(breakpoint, minMax = 'min') {
4
- // Handle string values from themes with units at the end
5
- if (typeof breakpoint === 'string') {
6
- breakpoint = parseInt(breakpoint, 10)
7
- }
8
-
9
- const matchMedia = React.useMemo(() => {
10
- if (typeof window === 'undefined') {
11
- const eventTarget = new EventTarget()
12
- eventTarget.matches = false
13
- return eventTarget
14
- }
15
- return window.matchMedia(`(${minMax}-width: ${breakpoint - (minMax === 'min' ? 0 : 1)}px)`)
16
- }, [breakpoint, minMax])
4
+const getMatches = query => (typeof window !== 'undefined' ? window.matchMedia(query).matches : false)
5
18
- const [matches, setMatches] = React.useState(matchMedia.matches)
19
- const handleChange = React.useCallback(() => setMatches(matchMedia.matches), [matchMedia])
6
+// The MIT License (MIT)
7
+// Copyright (c) 2020 Julien CARON
8
+// https://github.com/juliencrn/usehooks-ts/blob/master/packages/usehooks-ts/src/useMediaQuery/useMediaQuery.ts
9
+export function useMediaQuery(query) {
10
+ const [matches, setMatches] = React.useState(getMatches(query))
11
+ const handleChange = React.useCallback(() => setMatches(getMatches(query)), [query])
12
13
React.useEffect(() => {
14
+ handleChange()
15
+ const matchMedia = window.matchMedia(query)
16
matchMedia.addEventListener('change', handleChange)
17
return () => matchMedia.removeEventListener('change', handleChange)
24
- }, [matchMedia, handleChange])
18
+ }, [query, handleChange])
19
20
return matches
21
}
22
23
+export function useBreakpoint(breakpoint, minMax = 'min') {
24
+ // Handle string values from themes with units at the end
25
+ const px = typeof breakpoint === 'string' ? parseInt(breakpoint, 10) : breakpoint
26
+ return useMediaQuery(`(${minMax}-width: ${px - (minMax === 'min' ? 0 : 1)}px)`)
27
+}
28
+
29
+// a common breakpoint where things change on mobile
30
+export function useIsMobile() {
31
+ const theme = React.useContext(ThemeContext)
32
+ return useBreakpoint(theme.breakpoints[2], 'max')
33
+}
34
+
35
export default useBreakpoint
theme/src/use-search.js
+4
-1
@@ -1,8 +1,11 @@
1
import React from 'react'
2
import {useCombobox} from 'downshift'
3
import {navigate, graphql, useStaticQuery} from 'gatsby'
4
+import {useIsMobile} from './use-breakpoint'
5
+
6
+function useSearch() {
7
+ const isMobile = useIsMobile()
8
5
-function useSearch({isMobile = false} = {}) {
9
const queryRef = React.useRef()
10
const workerRef = React.useRef()
11