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