1import { css } from '@emotion/react';
2import { theme, typography } from '@expo/styleguide';
3import * as React from 'react';
4
5import { BASE_HEADING_LEVEL, Heading, HeadingType } from '~/common/headingManager';
6import { Tag } from '~/ui/components/Tag';
7import { MONOSPACE, CALLOUT } from '~/ui/components/Text';
8
9const STYLES_LINK = css`
10  transition: 50ms ease color;
11  display: flex;
12  text-decoration: none;
13  margin-bottom: 6px;
14  cursor: pointer;
15  overflow: hidden;
16  text-overflow: ellipsis;
17  align-items: center;
18  justify-content: space-between;
19`;
20
21const STYLES_LINK_LABEL = css`
22  overflow: hidden;
23  text-overflow: ellipsis;
24  white-space: nowrap;
25  color: ${theme.text.secondary};
26
27  :hover {
28    color: ${theme.link.default};
29  }
30`;
31
32const STYLES_LINK_MONOSPACE = css`
33  ${typography.fontSizes[13]}
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: ${theme.background.secondary};
44  max-width: 400px;
45  border: 1px solid ${theme.border.default};
46  padding: 3px 6px;
47  display: inline-block;
48`;
49
50const STYLES_TOOLTIP_TEXT = css`
51  ${typography.fontSizes[13]}
52  color: ${theme.text.default};
53  word-break: break-word;
54  word-wrap: normal;
55`;
56
57const STYLES_TOOLTIP_CODE = css`
58  ${typography.fontSizes[12]}
59`;
60
61const STYLES_TAG_CONTAINER = css`
62  display: inline-flex;
63`;
64
65const NESTING_OFFSET = 12;
66
67/**
68 * Replaces `Module.someFunction(arguments: argType)`
69 * with `someFunction()`
70 */
71const trimCodedTitle = (str: string) => {
72  const dotIdx = str.indexOf('.');
73  if (dotIdx > 0) str = str.substring(dotIdx + 1);
74
75  const parIdx = str.indexOf('(');
76  if (parIdx > 0) str = str.substring(0, parIdx + 1) + ')';
77
78  return str;
79};
80
81/**
82 * Determines if element is overflowing
83 * (its children width exceeds container width)
84 * @param {HTMLElement} el element to check
85 */
86const isOverflowing = (el: HTMLElement) => {
87  if (!el || !el.children) {
88    return false;
89  }
90
91  const childrenWidth = Array.from(el.children).reduce((sum, child) => sum + child.scrollWidth, 0);
92  return childrenWidth >= el.scrollWidth - parseInt(el.style.paddingLeft, 10);
93};
94
95type TooltipProps = React.PropsWithChildren<{
96  isCode?: boolean;
97  topOffset: number;
98}>;
99
100const Tooltip = ({ children, isCode, topOffset }: TooltipProps) => {
101  const ContentWrapper = isCode ? MONOSPACE : CALLOUT;
102  return (
103    <div css={STYLES_TOOLTIP} style={{ right: 24, top: topOffset }}>
104      <ContentWrapper css={[STYLES_TOOLTIP_TEXT, isCode && STYLES_TOOLTIP_CODE]}>
105        {children}
106      </ContentWrapper>
107    </div>
108  );
109};
110
111type SidebarLinkProps = {
112  heading: Heading;
113  isActive: boolean;
114  shortenCode: boolean;
115  onClick: (event: React.MouseEvent<HTMLAnchorElement>) => void;
116};
117
118const DocumentationSidebarRightLink = React.forwardRef<HTMLAnchorElement, SidebarLinkProps>(
119  ({ heading, isActive, shortenCode, onClick }, ref) => {
120    const { slug, level, title, type, tags } = heading;
121
122    const isCode = type === HeadingType.InlineCode;
123    const paddingLeft = NESTING_OFFSET * (level - BASE_HEADING_LEVEL);
124    const displayTitle = shortenCode && isCode ? trimCodedTitle(title) : title;
125
126    const [tooltipVisible, setTooltipVisible] = React.useState(false);
127    const [tooltipOffset, setTooltipOffset] = React.useState(-20);
128    const onMouseOver = (event: React.MouseEvent<HTMLAnchorElement>) => {
129      setTooltipVisible(isOverflowing(event.currentTarget));
130      setTooltipOffset(
131        event.currentTarget.getBoundingClientRect().top + event.currentTarget.offsetHeight
132      );
133    };
134
135    const onMouseOut = () => {
136      setTooltipVisible(false);
137    };
138
139    const TitleElement = isCode ? MONOSPACE : CALLOUT;
140
141    return (
142      <>
143        {tooltipVisible && (
144          <Tooltip topOffset={tooltipOffset} isCode={isCode}>
145            {displayTitle}
146          </Tooltip>
147        )}
148        <a
149          ref={ref}
150          onMouseOver={onMouseOver}
151          onMouseOut={onMouseOut}
152          href={'#' + slug}
153          onClick={onClick}
154          css={[STYLES_LINK, paddingLeft && { paddingLeft }]}>
155          <TitleElement
156            css={[
157              STYLES_LINK_LABEL,
158              isCode && STYLES_LINK_MONOSPACE,
159              isActive && STYLES_LINK_ACTIVE,
160            ]}>
161            {displayTitle}
162          </TitleElement>
163          {tags && tags.length ? (
164            <div css={STYLES_TAG_CONTAINER}>
165              {tags.map(tag => (
166                <Tag name={tag} type="toc" key={`${displayTitle}-${tag}`} />
167              ))}
168            </div>
169          ) : undefined}
170        </a>
171      </>
172    );
173  }
174);
175
176export default DocumentationSidebarRightLink;
177