From 50c7cd0e09f10a825d222d910184fef9abe8f471 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 2 Dec 2024 15:31:51 +0100 Subject: [PATCH] Experiment on at-mentions popover positioning --- src/sidebar/components/MarkdownEditor.tsx | 114 +++++++++++++++++++--- 1 file changed, 103 insertions(+), 11 deletions(-) diff --git a/src/sidebar/components/MarkdownEditor.tsx b/src/sidebar/components/MarkdownEditor.tsx index 9938b9bb1de..a37cfa842b1 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, @@ -183,17 +190,102 @@ function TextArea({ containerRef, ...restProps }: TextAreaProps & JSX.TextareaHTMLAttributes) { + const textareaRef = useSyncedRef(containerRef); + const anchorContainerRef = useRef(null); + const preAnchorRef = useRef(null); + const anchorRef = useRef(null); + const postAnchorRef = useRef(null); + const [open, setPopoverOpen] = useState(false); + + useEffect(() => { + const textarea = textareaRef.current!; + const anchorContainer = anchorContainerRef.current!; + const preAnchor = preAnchorRef.current!; + const anchor = anchorRef.current!; + const postAnchor = postAnchorRef.current!; + const listenerCollection = new ListenerCollection(); + + // Keep content in the anchor element in sync with the textarea + const keepContentInSync = (e: KeyboardEvent) => { + const typedKey = e.key; + + // TODO + // - When pressing backspace, check if we reach an at mention and open + // popover if so + // - When pressing backspace, check if we removed the at mention + // entirely, and close popover if so + // - Allow arrow-key navigation in suggestions, but without removing the + // focus from the textarea + + if (!open && typedKey === '@') { + // When '@' is typed while the popover was closed, sync content with the + // anchors and open popover + const value = textarea.value; + const atMentionStart = textarea.selectionStart - 1; + const atMentionEnd = atMentionStart + 1; + + const contentBeforeAtMention = value.substring(0, atMentionStart - 1); + const atMention = value.substring(atMentionStart, atMentionEnd); + const contentAfterAtMention = value.substring(atMentionEnd + 1); + + preAnchor.textContent = contentBeforeAtMention; + anchor.textContent = atMention; + postAnchor.textContent = contentAfterAtMention; + + // Right before opening the popover, make scrolls match + anchorContainer.scrollTop = textarea.scrollTop; + setPopoverOpen(true); + } else if (open && (typedKey === ' ' || typedKey === 'Enter')) { + // Close popover as soon as an empty character is written + setPopoverOpen(false); + } else if (open) { + // When the popover is open, append written characters to the anchor + anchor.textContent = `${anchor.textContent}${typedKey}`; + } + }; + listenerCollection.add(textarea, 'keydown', keepContentInSync); + + return () => { + listenerCollection.removeAll(); + }; + }, [anchorContainerRef, anchorRef, open, textareaRef]); + return ( -