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