1import { css } from '@emotion/react';
2import { theme, typography, LinkBase } from '@expo/styleguide';
3import { spacing } from '@expo/styleguide-base';
4import { ArrowUpRightIcon } from '@expo/styleguide-icons';
5import { useRouter } from 'next/compat/router';
6import type { PropsWithChildren } from 'react';
7import { useEffect, useRef } from 'react';
8
9import { stripVersionFromPath } from '~/common/utilities';
10import { NavigationRoute } from '~/types/common';
11
12type SidebarLinkProps = PropsWithChildren<{
13  info: NavigationRoute;
14}>;
15
16const HEAD_NAV_HEIGHT = 160;
17
18const isLinkInViewport = (element: HTMLAnchorElement) => {
19  const rect = element.getBoundingClientRect();
20  return (
21    rect.top - HEAD_NAV_HEIGHT >= 0 &&
22    rect.left >= 0 &&
23    rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
24    rect.right <= (window.innerWidth || document.documentElement.clientWidth)
25  );
26};
27
28export const SidebarLink = ({ info, children }: SidebarLinkProps) => {
29  const router = useRouter();
30  const ref = useRef<HTMLAnchorElement>(null);
31
32  const checkSelection = () => {
33    // Special case for root url
34    if (info.name === 'Introduction') {
35      if (router?.asPath.match(/\/versions\/[\w.]+\/$/) || router?.asPath === '/versions/latest/') {
36        return true;
37      }
38    }
39
40    const linkUrl = stripVersionFromPath(info.as || info.href);
41    return (
42      linkUrl === stripVersionFromPath(router?.pathname) ||
43      linkUrl === stripVersionFromPath(router?.asPath)
44    );
45  };
46
47  const isSelected = checkSelection();
48
49  useEffect(() => {
50    if (isSelected && ref?.current && !isLinkInViewport(ref?.current)) {
51      setTimeout(() => ref?.current && ref.current.scrollIntoView({ behavior: 'smooth' }), 50);
52    }
53  }, []);
54
55  if (info.hidden) {
56    return null;
57  }
58
59  const customDataAttributes = isSelected && {
60    'data-sidebar-anchor-selected': true,
61  };
62  const isExternal = info.href.startsWith('http');
63
64  return (
65    <div css={STYLES_CONTAINER}>
66      <LinkBase
67        href={info.href as string}
68        ref={ref}
69        css={[STYLES_LINK, isSelected && STYLES_LINK_ACTIVE]}
70        {...customDataAttributes}>
71        <div css={[STYLES_BULLET, isSelected && STYLES_ACTIVE_BULLET]} />
72        {children}
73        {isExternal && <ArrowUpRightIcon className="icon-sm text-icon-secondary ml-auto" />}
74      </LinkBase>
75    </div>
76  );
77};
78
79const STYLES_LINK = css`
80  ${typography.fontSizes[14]}
81  display: flex;
82  flex-direction: row;
83  text-decoration: none;
84  color: ${theme.text.secondary};
85  transition: 50ms ease color;
86  align-items: center;
87  scroll-margin: 60px;
88  width: 100%;
89  margin-left: -${spacing[2] + spacing[0.5]}px;
90
91  &:hover {
92    color: ${theme.text.link};
93  }
94
95  &:hover svg {
96    color: ${theme.button.tertiary.icon};
97  }
98`;
99
100const STYLES_LINK_ACTIVE = css`
101  color: ${theme.text.link};
102`;
103
104const STYLES_CONTAINER = css`
105  display: flex;
106  min-height: 32px;
107  align-items: center;
108  padding: ${spacing[1]}px;
109  padding-right: ${spacing[2]}px;
110`;
111
112const STYLES_BULLET = css`
113  height: 6px;
114  width: 6px;
115  min-height: 6px;
116  min-width: 6px;
117  border-radius: 100%;
118  margin: ${spacing[2]}px ${spacing[1.5]}px;
119  align-self: self-start;
120`;
121
122const STYLES_ACTIVE_BULLET = css`
123  background-color: ${theme.text.link};
124`;
125