Skip to content

Commit

Permalink
Display suggestions popover when at-mentioning
Browse files Browse the repository at this point in the history
  • Loading branch information
acelaya committed Dec 16, 2024
1 parent d0aed76 commit b397746
Showing 1 changed file with 73 additions and 10 deletions.
83 changes: 73 additions & 10 deletions src/sidebar/components/MarkdownEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { Button, IconButton, Link } from '@hypothesis/frontend-shared';
import {
Button,
IconButton,
Link,
Popover,
useSyncedRef,
} from '@hypothesis/frontend-shared';
import {
EditorLatexIcon,
EditorQuoteIcon,
Expand All @@ -16,6 +22,7 @@ import classnames from 'classnames';
import type { Ref, JSX } from 'preact';
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';

import { ListenerCollection } from '../../shared/listener-collection';
import { isMacOS } from '../../shared/user-agent';
import {
LinkType,
Expand All @@ -24,6 +31,7 @@ import {
toggleSpanStyle,
} from '../markdown-commands';
import type { EditorState } from '../markdown-commands';
import { useSidebarStore } from '../store';
import MarkdownView from './MarkdownView';

/**
Expand Down Expand Up @@ -173,6 +181,20 @@ function ToolbarButton({
);
}

/**
* Get the word right before the caret in a textarea.
* A word is anything delimited by spaces or newlines.
*/
function getTermBeforeCaret(textarea: HTMLTextAreaElement): string {
const caretPosition = textarea.selectionStart;
const textBeforeCaret = textarea.value
.slice(0, caretPosition)
.replace(/\n/g, ' ');
const lastSpaceIndex = textBeforeCaret.lastIndexOf(' ');

return textBeforeCaret.slice(lastSpaceIndex + 1);
}

type TextAreaProps = {
classes?: string;
containerRef?: Ref<HTMLTextAreaElement>;
Expand All @@ -183,17 +205,58 @@ function TextArea({
containerRef,
...restProps
}: TextAreaProps & JSX.TextareaHTMLAttributes<HTMLTextAreaElement>) {
const [popoverOpen, setPopoverOpen] = useState(false);
const textareaRef = useSyncedRef(containerRef);
const store = useSidebarStore();
const areMentionsEnabled = store.isFeatureEnabled('at_mentions');

useEffect(() => {
if (!areMentionsEnabled) {
return () => {};
}

const textarea = textareaRef.current!;
const listenerCollection = new ListenerCollection();

// We listen for `keyup` to make sure the text in the textarea reflects the
// just-pressed key when we evaluate it
listenerCollection.add(textarea, 'keyup', e => {
// `Esc` key is used to close the popover. Do nothing and let users close
// it that way, even if the caret is in a mention
if (e.key === 'Escape') {
return;
}
setPopoverOpen(getTermBeforeCaret(textarea).startsWith('@'));
});

return () => {
listenerCollection.removeAll();
};
}, [areMentionsEnabled, popoverOpen, textareaRef]);

return (
<textarea
className={classnames(
'border rounded p-2',
'text-color-text-light bg-grey-0',
'focus:bg-white focus:outline-none focus:shadow-focus-inner',
classes,
<div className="relative">
<textarea
className={classnames(
'border rounded p-2',
'text-color-text-light bg-grey-0',
'focus:bg-white focus:outline-none focus:shadow-focus-inner',
classes,
)}
{...restProps}
ref={textareaRef}
/>
{areMentionsEnabled && (
<Popover
open={popoverOpen}
onClose={() => setPopoverOpen(false)}
anchorElementRef={textareaRef}
classes="p-2"
>
Suggestions
</Popover>
)}
{...restProps}
ref={containerRef}
/>
</div>
);
}

Expand Down

0 comments on commit b397746

Please sign in to comment.