Skip to content

Commit

Permalink
Display a popover when hovering over highlighted mentions
Browse files Browse the repository at this point in the history
  • Loading branch information
acelaya committed Feb 12, 2025
1 parent 1e1103f commit 79e9832
Showing 1 changed file with 105 additions and 4 deletions.
109 changes: 105 additions & 4 deletions src/sidebar/components/MarkdownView.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,45 @@
import { useEffect, useMemo, useRef } from 'preact/hooks';
import { Popover } from '@hypothesis/frontend-shared';
import classnames from 'classnames';
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'preact/hooks';

import type { Mention } from '../../types/api';
import type { InvalidUsername } from '../helpers/mentions';
import { renderMentionTags } from '../helpers/mentions';
import { replaceLinksWithEmbeds } from '../media-embedder';
import { renderMathAndMarkdown } from '../render-markdown';
import StyledText from './StyledText';

function MentionPopoverContent({ mention }: { mention: Mention }) {
return (

Check warning on line 19 in src/sidebar/components/MarkdownView.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/components/MarkdownView.tsx#L18-L19

Added lines #L18 - L19 were not covered by tests
<div className="flex flex-col gap-y-1.5">
<div className="text-md font-bold">@{mention.username}</div>
{mention.display_name && (
<div className="text-color-text-light">{mention.display_name}</div>

Check warning on line 23 in src/sidebar/components/MarkdownView.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/components/MarkdownView.tsx#L22-L23

Added lines #L22 - L23 were not covered by tests
)}
</div>
);
}

export type MarkdownViewProps = {
/** The string of markdown to display as HTML. */
markdown: string;
classes?: string;
style?: Record<string, string>;
mentions?: Mention[];

// Test seams
setTimeout_?: typeof setTimeout;
clearTimeout_?: typeof clearTimeout;
};

type PopoverContent = Mention | InvalidUsername | null;

/**
* A component which renders markdown as HTML and replaces recognized links
* with embedded video/audio.
Expand All @@ -22,14 +48,49 @@ export default function MarkdownView({
markdown,
classes,
style,
mentions,
mentions = [],
/* istanbul ignore next - test seam */
setTimeout_ = setTimeout,
/* istanbul ignore next - test seam */
clearTimeout_ = clearTimeout,
}: MarkdownViewProps) {
const html = useMemo(
() => (markdown ? renderMathAndMarkdown(markdown) : ''),
[markdown],
);
const content = useRef<HTMLDivElement | null>(null);

const mentionsPopoverAnchorRef = useRef<HTMLElement | null>(null);
const elementToMentionMap = useRef(
new Map<HTMLElement, Mention | InvalidUsername>(),
);
const [popoverContent, doSetPopoverContent] = useState<PopoverContent>(null);
/**
* This allows the content to be set with a small delay, so that popovers don't flickr
*/
const popoverContentTimeout = useRef<ReturnType<typeof setTimeout> | null>();
const setPopoverContent = useCallback(
(content: PopoverContent) => {
if (popoverContentTimeout.current) {
clearTimeout_(popoverContentTimeout.current);

Check warning on line 75 in src/sidebar/components/MarkdownView.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/components/MarkdownView.tsx#L73-L75

Added lines #L73 - L75 were not covered by tests
}

const setContent = () => {
doSetPopoverContent(content);
popoverContentTimeout.current = null;

Check warning on line 80 in src/sidebar/components/MarkdownView.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/components/MarkdownView.tsx#L78-L80

Added lines #L78 - L80 were not covered by tests
};

// Set the content immediately when resetting, so that there's no delay
// when hiding the popover, only when showing it
if (content === null) {
setContent();
} else {
popoverContentTimeout.current = setTimeout_(setContent, 400);

Check warning on line 88 in src/sidebar/components/MarkdownView.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/components/MarkdownView.tsx#L85-L88

Added lines #L85 - L88 were not covered by tests
}
},
[clearTimeout_, setTimeout_],
);

useEffect(() => {
replaceLinksWithEmbeds(content.current!, {
// Make embeds the full width of the sidebar, unless the sidebar has been
Expand All @@ -40,7 +101,7 @@ export default function MarkdownView({
}, [markdown]);

useEffect(() => {
renderMentionTags(content.current!, mentions ?? []);
elementToMentionMap.current = renderMentionTags(content.current!, mentions);
}, [mentions]);

// NB: The following could be implemented by setting attribute props directly
Expand All @@ -50,14 +111,54 @@ export default function MarkdownView({
// a review in the future.
return (
<div className="w-full break-anywhere cursor-text">
<StyledText>
<StyledText
classes={classnames(
// A `relative` wrapper around the `Popover` component is needed for
// when the native Popover API is not supported.
'relative',
)}
>
<div
className={classes}
data-testid="markdown-text"
ref={content}
data-foo="foo"
dangerouslySetInnerHTML={{ __html: html }}
style={style}
// React types do not define `onMouseEnterCapture`, but preact does
// eslint-disable-next-line react/no-unknown-property
onMouseEnterCapture={({ target }) => {
const element = target as HTMLElement;
const mention = elementToMentionMap.current.get(element);

Check warning on line 132 in src/sidebar/components/MarkdownView.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/components/MarkdownView.tsx#L130-L132

Added lines #L130 - L132 were not covered by tests

if (mention) {
setPopoverContent(mention);
mentionsPopoverAnchorRef.current = element;

Check warning on line 136 in src/sidebar/components/MarkdownView.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/components/MarkdownView.tsx#L134-L136

Added lines #L134 - L136 were not covered by tests
}
}}
// React types do not define `onMouseLeaveCapture`, but preact does
// eslint-disable-next-line react/no-unknown-property
onMouseLeaveCapture={() => {
setPopoverContent(null);
mentionsPopoverAnchorRef.current = null;

Check warning on line 143 in src/sidebar/components/MarkdownView.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/components/MarkdownView.tsx#L141-L143

Added lines #L141 - L143 were not covered by tests
}}
/>
<Popover
open={!!popoverContent}
onClose={() => setPopoverContent(null)}

Check warning on line 148 in src/sidebar/components/MarkdownView.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/components/MarkdownView.tsx#L148

Added line #L148 was not covered by tests
anchorElementRef={mentionsPopoverAnchorRef}
classes="px-3 py-2"
>
{typeof popoverContent === 'string' && (
<>

Check warning on line 153 in src/sidebar/components/MarkdownView.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/components/MarkdownView.tsx#L153

Added line #L153 was not covered by tests
No user with username{' '}
<span className="font-bold">{popoverContent}</span> exists
</>
)}
{popoverContent !== null && typeof popoverContent === 'object' && (
<MentionPopoverContent mention={popoverContent} />

Check warning on line 159 in src/sidebar/components/MarkdownView.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/components/MarkdownView.tsx#L159

Added line #L159 was not covered by tests
)}
</Popover>
</StyledText>
</div>
);
Expand Down

0 comments on commit 79e9832

Please sign in to comment.