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