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