From 07ebc158339d9dfe51714eb4fd08cf19ecf7c8c6 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 16 Dec 2024 15:54:17 +0100 Subject: [PATCH] Display suggestions popover when at-mentioning --- .../Annotation/AnnotationEditor.tsx | 1 + .../Annotation/test/AnnotationEditor-test.js | 1 + src/sidebar/components/MarkdownEditor.tsx | 81 ++++++++++++-- .../components/test/MarkdownEditor-test.js | 103 +++++++++++++++++- src/sidebar/util/term-before-position.ts | 9 ++ .../util/test/term-before-position-test.js | 72 ++++++++++++ 6 files changed, 256 insertions(+), 11 deletions(-) create mode 100644 src/sidebar/util/term-before-position.ts create mode 100644 src/sidebar/util/test/term-before-position-test.js diff --git a/src/sidebar/components/Annotation/AnnotationEditor.tsx b/src/sidebar/components/Annotation/AnnotationEditor.tsx index 3c75a54641c..30d76ec2593 100644 --- a/src/sidebar/components/Annotation/AnnotationEditor.tsx +++ b/src/sidebar/components/Annotation/AnnotationEditor.tsx @@ -179,6 +179,7 @@ function AnnotationEditor({ label={isReplyAnno ? 'Enter reply' : 'Enter comment'} text={text} onEditText={onEditText} + atMentionsEnabled={store.isFeatureEnabled('at_mentions')} /> { setDefault: sinon.stub(), removeDraft: sinon.stub(), removeAnnotations: sinon.stub(), + isFeatureEnabled: sinon.stub().returns(false), }; $imports.$mock(mockImportedComponents()); diff --git a/src/sidebar/components/MarkdownEditor.tsx b/src/sidebar/components/MarkdownEditor.tsx index 9938b9bb1de..d3b6502c317 100644 --- a/src/sidebar/components/MarkdownEditor.tsx +++ b/src/sidebar/components/MarkdownEditor.tsx @@ -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, @@ -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, @@ -24,6 +31,7 @@ import { toggleSpanStyle, } from '../markdown-commands'; import type { EditorState } from '../markdown-commands'; +import { termBeforePosition } from '../util/term-before-position'; import MarkdownView from './MarkdownView'; /** @@ -176,24 +184,69 @@ function ToolbarButton({ type TextAreaProps = { classes?: string; containerRef?: Ref; + atMentionsEnabled: boolean; }; function TextArea({ classes, containerRef, + atMentionsEnabled, ...restProps }: TextAreaProps & JSX.TextareaHTMLAttributes) { + const [popoverOpen, setPopoverOpen] = useState(false); + const textareaRef = useSyncedRef(containerRef); + + useEffect(() => { + if (!atMentionsEnabled) { + 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( + termBeforePosition(textarea.value, textarea.selectionStart).startsWith( + '@', + ), + ); + }); + + return () => { + listenerCollection.removeAll(); + }; + }, [atMentionsEnabled, popoverOpen, textareaRef]); + return ( -