1import { css } from '@emotion/react'; 2import { theme, iconSize, spacing, ChevronDownIcon, borderRadius, shadows } from '@expo/styleguide'; 3import { useRouter } from 'next/router'; 4import React, { useEffect, useRef, useState } from 'react'; 5 6import { ButtonBase } from '../Button'; 7import { CALLOUT } from '../Text'; 8 9import stripVersionFromPath from '~/common/stripVersionFromPath'; 10import { NavigationRoute } from '~/types/common'; 11 12if (typeof window !== 'undefined' && !window.hasOwnProperty('sidebarState')) { 13 window.sidebarState = {}; 14} 15 16type Props = React.PropsWithChildren<{ 17 info: NavigationRoute; 18}>; 19 20export function SidebarCollapsible(props: Props) { 21 const { info, children } = props; 22 const router = useRouter(); 23 const ref = useRef<HTMLButtonElement>(null); 24 25 const isChildRouteActive = () => { 26 let result = false; 27 28 const sections = info.children; 29 30 const isSectionActive = (section: NavigationRoute) => { 31 const linkUrl = stripVersionFromPath(section.as || section.href); 32 const pathname = stripVersionFromPath(router.pathname); 33 const asPath = stripVersionFromPath(router.asPath); 34 35 if (linkUrl === pathname || linkUrl === asPath) { 36 result = true; 37 } 38 }; 39 40 const posts: NavigationRoute[] = 41 sections 42 ?.map(section => (section.type === 'page' ? [section] : section?.children ?? [])) 43 .flat() ?? []; 44 45 posts.forEach(isSectionActive); 46 return result; 47 }; 48 49 const hasCachedState = 50 typeof window !== 'undefined' && window.sidebarState[info.name] !== undefined; 51 52 const containsActiveChild = isChildRouteActive(); 53 const initState = hasCachedState 54 ? window.sidebarState[info.name] 55 : containsActiveChild || info.expanded; 56 57 const [isOpen, setOpen] = useState(initState); 58 59 useEffect(() => { 60 if (containsActiveChild) { 61 window.sidebarState[info.name] = true; 62 } 63 }, []); 64 65 const toggleIsOpen = () => { 66 setOpen(prevState => !prevState); 67 window.sidebarState[info.name] = !isOpen; 68 }; 69 70 const customDataAttributes = containsActiveChild && { 71 'data-collapsible-active': true, 72 }; 73 74 return ( 75 <> 76 <ButtonBase 77 ref={ref} 78 css={titleStyle} 79 aria-expanded={isOpen ? 'true' : 'false'} 80 onClick={toggleIsOpen} 81 {...customDataAttributes}> 82 <div css={chevronContainerStyle}> 83 <ChevronDownIcon 84 size={iconSize.tiny} 85 css={[chevronStyle, !isOpen && chevronClosedStyle]} 86 /> 87 </div> 88 <CALLOUT weight="medium">{info.name}</CALLOUT> 89 </ButtonBase> 90 {isOpen && <div aria-hidden={!isOpen ? 'true' : 'false'}>{children}</div>} 91 </> 92 ); 93} 94 95const titleStyle = css({ 96 display: 'flex', 97 alignItems: 'center', 98 gap: spacing[1.5], 99 position: 'relative', 100 marginBottom: spacing[2], 101 userSelect: 'none', 102 transition: '100ms', 103 padding: `${spacing[1.5]}px ${spacing[3]}px`, 104 width: '100%', 105 106 ':hover': { 107 cursor: 'pointer', 108 backgroundColor: theme.background.tertiary, 109 borderRadius: borderRadius.medium, 110 transition: '100ms', 111 }, 112}); 113 114const chevronContainerStyle = css({ 115 backgroundColor: theme.background.default, 116 border: `1px solid ${theme.border.default}`, 117 borderRadius: borderRadius.small, 118 display: 'flex', 119 alignItems: 'center', 120 justifyContent: 'center', 121 boxShadow: shadows.micro, 122 height: 20, 123 width: 20, 124 marginRight: spacing[1], 125}); 126 127const chevronStyle = css({ 128 transition: '100ms ease transform', 129 transform: 'translateX(-0.5px)', 130}); 131 132const chevronClosedStyle = css({ 133 transform: 'rotate(-90deg)', 134}); 135