xref: /expo/docs/components/Permalink.tsx (revision 3a141dca)
1import { css } from '@emotion/react';
2import { LinkBase } from '@expo/styleguide';
3import * as React from 'react';
4import tippy, { roundArrow } from 'tippy.js';
5
6import { AdditionalProps } from '~/common/headingManager';
7import PermalinkIcon from '~/components/icons/Permalink';
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 = ({ slug }: { slug: string }) => {
81  const tippyFunc = testTippy || tippy;
82  React.useEffect(() => {
83    tippyFunc('#docs-anchor-permalink-' + slug, {
84      content: 'Click to copy anchor link',
85      arrow: roundArrow,
86      offset: [0, 0],
87    });
88  }, []);
89
90  return (
91    <span
92      id={'docs-anchor-permalink-' + slug}
93      onClick={event => {
94        event.preventDefault();
95        const url = window.location.href.replace(/#.*/, '') + '#' + slug;
96        navigator.clipboard?.writeText(url);
97      }}
98      css={STYLES_PERMALINK_ICON}>
99      <PermalinkIcon />
100    </span>
101  );
102};
103
104export { PermalinkCopyIcon };
105
106const Permalink: React.FC<EnhancedProps> = withHeadingManager(
107  (props: EnhancedProps & HeadingManagerProps) => {
108    // NOTE(jim): Not the greatest way to generate permalinks.
109    // for now I've shortened the length of permalinks.
110    const component = props.children as JSX.Element;
111    const children = component.props.children || '';
112
113    if (!props.nestingLevel) {
114      return children;
115    }
116
117    const heading = props.headingManager.addHeading(
118      children,
119      props.nestingLevel,
120      props.additionalProps,
121      props.id
122    );
123
124    return (
125      <PermalinkBase component={component} style={props.additionalProps?.style}>
126        <LinkBase css={STYLES_PERMALINK_LINK} href={'#' + heading.slug} ref={heading.ref}>
127          <span css={STYLES_PERMALINK_TARGET} id={heading.slug} />
128          <span css={STYLED_PERMALINK_CONTENT}>{children}</span>
129          <PermalinkCopyIcon slug={heading.slug} />
130        </LinkBase>
131      </PermalinkBase>
132    );
133  }
134);
135
136export default Permalink;
137