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/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 { asPath, pathname } = useRouter();
30  const ref = useRef<HTMLAnchorElement>(null);
31
32  const checkSelection = () => {
33    // Special case for root url
34    if (info.name === 'Introduction') {
35      if (asPath.match(/\/versions\/[\w.]+\/$/) || asPath === '/versions/latest/') {
36        return true;
37      }
38    }
39
40    const linkUrl = stripVersionFromPath(info.as || info.href);
41    return linkUrl === stripVersionFromPath(pathname) || linkUrl === stripVersionFromPath(asPath);
42  };
43
44  const isSelected = checkSelection();
45
46  useEffect(() => {
47    if (isSelected && ref?.current && !isLinkInViewport(ref?.current)) {
48      setTimeout(() => ref?.current && ref.current.scrollIntoView({ behavior: 'smooth' }), 50);
49    }
50  }, []);
51
52  if (info.hidden) {
53    return null;
54  }
55
56  const customDataAttributes = isSelected && {
57    'data-sidebar-anchor-selected': true,
58  };
59  const isExternal = info.href.startsWith('http');
60
61  return (
62    <div css={STYLES_CONTAINER}>
63      <LinkBase
64        href={info.href as string}
65        ref={ref}
66        css={[STYLES_LINK, isSelected && STYLES_LINK_ACTIVE]}
67        {...customDataAttributes}>
68        {isSelected && <div css={STYLES_ACTIVE_BULLET} />}
69        {children}
70        {isExternal && <ArrowUpRightIcon className="icon-sm text-icon-secondary ml-auto" />}
71      </LinkBase>
72    </div>
73  );
74};
75
76const STYLES_LINK = css`
77  ${typography.fontSizes[14]}
78  display: flex;
79  flex-direction: row;
80  text-decoration: none;
81  color: ${theme.text.secondary};
82  transition: 50ms ease color;
83  align-items: center;
84  padding-left: ${spacing[4] + spacing[0.5]}px;
85  scroll-margin: 60px;
86  width: 100%;
87
88  &:hover {
89    color: ${theme.text.link};
90  }
91
92  &:hover svg {
93    color: ${theme.button.tertiary.icon};
94  }
95`;
96
97const STYLES_LINK_ACTIVE = css`
98  color: ${theme.text.link};
99  padding-left: 0;
100`;
101
102const STYLES_CONTAINER = css`
103  display: flex;
104  min-height: 32px;
105  align-items: center;
106  padding: ${spacing[1]}px;
107  padding-right: ${spacing[2]}px;
108`;
109
110const STYLES_ACTIVE_BULLET = css`
111  height: 6px;
112  width: 6px;
113  min-height: 6px;
114  min-width: 6px;
115  background-color: ${theme.text.link};
116  border-radius: 100%;
117  margin: ${spacing[2]}px ${spacing[1.5]}px;
118`;
119