Skip to content

Commit

Permalink
Experiment on at-mentions popover positioning
Browse files Browse the repository at this point in the history
  • Loading branch information
acelaya committed Dec 5, 2024
1 parent c738173 commit 50c7cd0
Showing 1 changed file with 103 additions and 11 deletions.
114 changes: 103 additions & 11 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 Down Expand Up @@ -183,17 +190,102 @@ function TextArea({
containerRef,
...restProps
}: TextAreaProps & JSX.TextareaHTMLAttributes<HTMLTextAreaElement>) {
const textareaRef = useSyncedRef(containerRef);
const anchorContainerRef = useRef<HTMLDivElement | null>(null);
const preAnchorRef = useRef<HTMLSpanElement | null>(null);
const anchorRef = useRef<HTMLSpanElement | null>(null);
const postAnchorRef = useRef<HTMLSpanElement | null>(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;

Check warning on line 225 in src/sidebar/components/MarkdownEditor.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/components/MarkdownEditor.tsx#L223-L225

Added lines #L223 - L225 were not covered by tests

const contentBeforeAtMention = value.substring(0, atMentionStart - 1);
const atMention = value.substring(atMentionStart, atMentionEnd);
const contentAfterAtMention = value.substring(atMentionEnd + 1);

Check warning on line 229 in src/sidebar/components/MarkdownEditor.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/components/MarkdownEditor.tsx#L227-L229

Added lines #L227 - L229 were not covered by tests

preAnchor.textContent = contentBeforeAtMention;
anchor.textContent = atMention;
postAnchor.textContent = contentAfterAtMention;

Check warning on line 233 in src/sidebar/components/MarkdownEditor.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/components/MarkdownEditor.tsx#L231-L233

Added lines #L231 - L233 were not covered by tests

// Right before opening the popover, make scrolls match
anchorContainer.scrollTop = textarea.scrollTop;
setPopoverOpen(true);

Check warning on line 237 in src/sidebar/components/MarkdownEditor.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/components/MarkdownEditor.tsx#L236-L237

Added lines #L236 - L237 were not covered by tests
} else if (open && (typedKey === ' ' || typedKey === 'Enter')) {
// Close popover as soon as an empty character is written
setPopoverOpen(false);

Check warning on line 240 in src/sidebar/components/MarkdownEditor.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/components/MarkdownEditor.tsx#L240

Added line #L240 was not covered by tests
} else if (open) {
// When the popover is open, append written characters to the anchor
anchor.textContent = `${anchor.textContent}${typedKey}`;

Check warning on line 243 in src/sidebar/components/MarkdownEditor.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/components/MarkdownEditor.tsx#L243

Added line #L243 was not covered by tests
}
};
listenerCollection.add(textarea, 'keydown', keepContentInSync);

return () => {
listenerCollection.removeAll();
};
}, [anchorContainerRef, anchorRef, open, 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,
)}
{...restProps}
ref={containerRef}
/>
<>
<div className="relative">
<div
className={classnames(
// Consistent dimensions, font size and line height with textarea
'border p-2 text-base touch:text-touch-base',
'invisible absolute top-0 left-0 right-0 h-[calc(100%-2px)] -z-1',
'overflow-y-auto whitespace-pre-wrap break-words',
)}
ref={anchorContainerRef}
>
<span ref={preAnchorRef} />
<span ref={anchorRef} />
<span ref={postAnchorRef} />
</div>
<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}
/>
<Popover
open={open}
anchorElementRef={anchorRef}
onClose={() => setPopoverOpen(false)}

Check warning on line 282 in src/sidebar/components/MarkdownEditor.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/components/MarkdownEditor.tsx#L282

Added line #L282 was not covered by tests
classes="p-2"
>
Suggested users
</Popover>
</div>
</>
);
}

Expand Down

0 comments on commit 50c7cd0

Please sign in to comment.