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 11, 2025
1 parent 2f22ac3 commit 21c7577
Showing 1 changed file with 74 additions and 4 deletions.
78 changes: 74 additions & 4 deletions src/sidebar/components/MarkdownView.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import { useEffect, useMemo, useRef } from 'preact/hooks';
import { ListenerCollection, Popover } from '@hypothesis/frontend-shared';
import classnames from 'classnames';
import { 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 (
<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>
)}
</div>
);
}

export type MarkdownViewProps = {
/** The string of markdown to display as HTML. */
markdown: string;
Expand All @@ -22,14 +36,19 @@ export default function MarkdownView({
markdown,
classes,
style,
mentions,
mentions = [],
}: MarkdownViewProps) {
const html = useMemo(
() => (markdown ? renderMathAndMarkdown(markdown) : ''),
[markdown],
);
const content = useRef<HTMLDivElement | null>(null);

const [popoverContent, setPopoverContent] = useState<
Mention | InvalidUsername | null
>(null);
const mentionsPopoverAnchorRef = useRef<HTMLElement | null>(null);

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

useEffect(() => {
renderMentionTags(content.current!, mentions ?? []);
const listenerCollection = new ListenerCollection();
const foundMentions = renderMentionTags(content.current!, mentions);

listenerCollection.add(
content.current!,
'mouseenter',
({ target }) => {
const element = target as HTMLElement;
const mention = foundMentions.get(element) ?? null;

if (mention) {
setPopoverContent(mention);
mentionsPopoverAnchorRef.current = element;
}
},
{ capture: true },
);
listenerCollection.add(
content.current!,
'mouseleave',
() => {
setPopoverContent(null);
mentionsPopoverAnchorRef.current = null;
},
{ capture: true },
);

return () => listenerCollection.removeAll();
}, [mentions]);

// NB: The following could be implemented by setting attribute props directly
Expand All @@ -50,14 +96,38 @@ 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}
dangerouslySetInnerHTML={{ __html: html }}
style={style}
/>
{mentions.length > 0 && (
<Popover
open={!!popoverContent}
onClose={() => setPopoverContent(null)}
anchorElementRef={mentionsPopoverAnchorRef}
classes="px-3 py-2"
>
{typeof popoverContent === 'string' && (
<>
No user with username{' '}
<span className="font-bold">{popoverContent}</span> exists
</>
)}
{popoverContent !== null && typeof popoverContent === 'object' && (
<MentionPopoverContent mention={popoverContent} />
)}
</Popover>
)}
</StyledText>
</div>
);
Expand Down

0 comments on commit 21c7577

Please sign in to comment.