1import { css } from '@emotion/react';
2import { theme, palette, typography } from '@expo/styleguide';
3import * as React from 'react';
4
5import { BASE_HEADING_LEVEL, Heading, HeadingType } from '../common/headingManager';
6
7import { paragraph } from '~/components/base/typography';
8
9const STYLES_LINK = css`
10  ${paragraph}
11  color: ${theme.text.secondary};
12  transition: 50ms ease color;
13  font-size: 14px;
14  display: block;
15  text-decoration: none;
16  margin-bottom: 6px;
17  cursor: pointer;
18
19  :hover {
20    color: ${theme.link.default};
21  }
22`;
23
24const STYLES_LINK_HEADER = css`
25  font-family: ${typography.fontFaces.medium};
26`;
27
28const STYLES_LINK_CODE = css`
29  font-family: ${typography.fontFaces.mono};
30  font-size: 13px;
31  overflow: hidden;
32  text-overflow: ellipsis;
33  white-space: nowrap;
34`;
35
36const STYLES_LINK_ACTIVE = css`
37  color: ${theme.link.default};
38`;
39
40const STYLES_TOOLTIP = css`
41  border-radius: 3px;
42  position: absolute;
43  background-color: ${palette.dark.white};
44  font-family: ${typography.fontFaces.medium};
45  max-width: 400px;
46  border: 1px solid black;
47  padding: 3px 6px;
48  letter-spacing: normal;
49  line-height: 1.4;
50  word-break: break-word;
51  word-wrap: normal;
52  font-size: 12px;
53
54  display: inline-block;
55`;
56
57const STYLES_CODE_TOOLTIP = css`
58  font-family: ${typography.fontFaces.mono};
59  font-size: 11px;
60`;
61
62const NESTING_OFFSET = 16;
63
64/**
65 * Replaces `Module.someFunction(arguments: argType)`
66 * with `someFunction()`
67 */
68const trimCodedTitle = (str: string) => {
69  const dotIdx = str.indexOf('.');
70  if (dotIdx > 0) str = str.substring(dotIdx + 1);
71
72  const parIdx = str.indexOf('(');
73  if (parIdx > 0) str = str.substring(0, parIdx + 1) + ')';
74
75  return str;
76};
77
78/**
79 * Determines if element is overflowing
80 * (its width exceeds container width)
81 * @param {HTMLElement} el element to check
82 */
83const isOverflowing = (el: HTMLElement) => {
84  if (!el) {
85    return false;
86  }
87
88  return el.clientWidth < el.scrollWidth;
89};
90
91type TooltipProps = {
92  isCode?: boolean;
93  topOffset: number;
94};
95
96const Tooltip: React.FC<TooltipProps> = ({ children, isCode, topOffset }) => (
97  <div css={[STYLES_TOOLTIP, isCode && STYLES_CODE_TOOLTIP]} style={{ right: 20, top: topOffset }}>
98    {children}
99  </div>
100);
101
102type SidebarLinkProps = {
103  heading: Heading;
104  isActive: boolean;
105  shortenCode: boolean;
106  onClick: (event: React.MouseEvent<HTMLAnchorElement>) => void;
107};
108
109const DocumentationSidebarRightLink = React.forwardRef<HTMLAnchorElement, SidebarLinkProps>(
110  ({ heading, isActive, shortenCode, onClick }, ref) => {
111    const { slug, level, title, type } = heading;
112
113    const isNested = level <= BASE_HEADING_LEVEL;
114    const isCode = type === HeadingType.InlineCode;
115
116    const paddingLeft = NESTING_OFFSET * (level - BASE_HEADING_LEVEL) + 'px';
117    const displayTitle = shortenCode && isCode ? trimCodedTitle(title) : title;
118
119    const [tooltipVisible, setTooltipVisible] = React.useState(false);
120    const [tooltipOffset, setTooltipOffset] = React.useState(-20);
121    const onMouseOver = (event: React.MouseEvent<HTMLAnchorElement>) => {
122      setTooltipVisible(isOverflowing(event.currentTarget));
123      setTooltipOffset(event.currentTarget.getBoundingClientRect().top + 25);
124    };
125    const onMouseOut = () => {
126      setTooltipVisible(false);
127    };
128
129    return (
130      <>
131        {tooltipVisible && (
132          <Tooltip topOffset={tooltipOffset} isCode={isCode}>
133            {displayTitle}
134          </Tooltip>
135        )}
136        <a
137          ref={ref}
138          onMouseOver={onMouseOver}
139          onMouseOut={onMouseOut}
140          style={{ paddingLeft }}
141          href={'#' + slug}
142          onClick={onClick}
143          css={[
144            STYLES_LINK,
145            isNested && STYLES_LINK_HEADER,
146            isCode && STYLES_LINK_CODE,
147            isActive && STYLES_LINK_ACTIVE,
148          ]}>
149          {displayTitle}
150        </a>
151      </>
152    );
153  }
154);
155
156export default DocumentationSidebarRightLink;
157