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