xref: /expo/docs/components/Permalink.tsx (revision 623daeb3)
1import { css } from '@emotion/react';
2import { LinkBase } from '@expo/styleguide';
3import { ClipboardIcon, Link01SolidIcon } from '@expo/styleguide-icons';
4import * as React from 'react';
5import tippy from 'tippy.js';
6
7import { AdditionalProps } from '~/common/headingManager';
8import withHeadingManager, {
9  HeadingManagerProps,
10} from '~/components/page-higher-order/withHeadingManager';
11
12type BaseProps = React.PropsWithChildren<{
13  component: any;
14  className?: string;
15  style?: React.CSSProperties;
16}>;
17
18type EnhancedProps = React.PropsWithChildren<{
19  // Sidebar heading level override
20  nestingLevel?: number;
21  additionalProps?: AdditionalProps;
22  id?: string;
23}>;
24
25const STYLES_PERMALINK_TARGET = css`
26  display: block;
27  position: absolute;
28  top: -46px;
29  visibility: hidden;
30`;
31
32const STYLES_PERMALINK_LINK = css`
33  position: relative;
34  color: inherit;
35  text-decoration: none !important;
36
37  /* Disable link when used in collapsible, to allow expand on click */
38  details & {
39    pointer-events: none;
40  }
41`;
42
43const STYLED_PERMALINK_CONTENT = css`
44  display: inline;
45`;
46
47const STYLES_PERMALINK_ICON = css`
48  cursor: pointer;
49  vertical-align: middle;
50  display: inline-block;
51  width: 1.2em;
52  height: 1em;
53  padding: 0 0.2em;
54  visibility: hidden;
55
56  a:hover &,
57  a:focus-visible & {
58    visibility: visible;
59  }
60
61  svg {
62    width: 100%;
63    height: auto;
64  }
65`;
66
67const PermalinkBase = ({ component, children, className, ...rest }: BaseProps) =>
68  React.cloneElement(
69    component,
70    {
71      className: [className, component.props.className || ''].join(' '),
72      ...rest,
73    },
74    children
75  );
76
77// @ts-ignore Jest ESM issue https://github.com/facebook/jest/issues/9430
78const { default: testTippy } = tippy;
79
80const PermalinkCopyIcon = ({
81  slug,
82  onClick,
83}: {
84  slug: string;
85  onClick?: (event: React.MouseEvent<HTMLElement>) => void | undefined;
86}) => {
87  const tippyFunc = testTippy || tippy;
88
89  const [tooltipInstance, setTooltipInstance] = React.useState<
90    { setContent: (content: string) => void } | undefined
91  >(undefined);
92
93  React.useEffect(() => {
94    const tippyInstance = tippyFunc('#docs-anchor-permalink-copy-' + slug, {
95      content: 'Copy anchor link',
96      arrow: null,
97      offset: [0, 0],
98      hideOnClick: false,
99    });
100    setTooltipInstance(tippyInstance[0]);
101  }, []);
102
103  const myOnClick = React.useCallback(
104    (event: React.MouseEvent<HTMLElement>) => {
105      event.preventDefault();
106      const url = window.location.href.replace(/#.*/, '') + '#' + slug;
107      navigator.clipboard?.writeText(url);
108      tooltipInstance?.setContent('Copied!');
109      onClick && onClick(event);
110    },
111    [tooltipInstance, onClick]
112  );
113
114  return (
115    <span id={'docs-anchor-permalink-copy-' + slug} onClick={myOnClick} css={STYLES_PERMALINK_ICON}>
116      <ClipboardIcon className="icon-sm text-icon-default" />
117    </span>
118  );
119};
120
121export { PermalinkCopyIcon };
122
123const Permalink: React.FC<EnhancedProps> = withHeadingManager(
124  (props: EnhancedProps & HeadingManagerProps) => {
125    // NOTE(jim): Not the greatest way to generate permalinks.
126    // for now I've shortened the length of permalinks.
127    const component = props.children as JSX.Element;
128    const children = component.props.children || '';
129
130    if (!props.nestingLevel) {
131      return children;
132    }
133
134    const heading = props.headingManager.addHeading(
135      children,
136      props.nestingLevel,
137      props.additionalProps,
138      props.id
139    );
140
141    return (
142      <PermalinkBase component={component} style={props.additionalProps?.style}>
143        <LinkBase css={STYLES_PERMALINK_LINK} href={'#' + heading.slug} ref={heading.ref}>
144          <span css={STYLES_PERMALINK_TARGET} id={heading.slug} />
145          <span css={STYLED_PERMALINK_CONTENT}>{children}</span>
146          <span css={STYLES_PERMALINK_ICON}>
147            <Link01SolidIcon className="icon-sm text-icon-default" />
148          </span>
149          <PermalinkCopyIcon slug={heading.slug} />
150        </LinkBase>
151      </PermalinkBase>
152    );
153  }
154);
155
156export default Permalink;
157