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