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