v3: added emoji picker, various bug and layout fixes

This commit is contained in:
khannurien
2026-03-22 20:24:29 +00:00
parent a104113e05
commit c5051e3485
24 changed files with 384 additions and 177 deletions

View File

@@ -0,0 +1,79 @@
import { type RefObject, useCallback, useRef, useState } from "react";
// Trigger: ':' not preceded by a word character, followed by 1+ word chars
const TRIGGER_RE = /(?<![A-Za-z0-9_]):([A-Za-z0-9_+\-]{1,})$/;
interface EmojiTriggerState {
open: boolean;
query: string;
}
export function useEmojiTrigger(
value: string,
onChange: (value: string) => void,
textareaRef: RefObject<HTMLTextAreaElement | null>,
) {
const [state, setState] = useState<EmojiTriggerState>({
open: false,
query: "",
});
// Saved positions at the moment the trigger was detected
const triggerStartRef = useRef(0);
const triggerCursorRef = useRef(0);
/**
* Call this from the textarea's onChange (alongside the mention handler).
* It receives the new value and cursor position already read from the event.
*/
const detectEmojiTrigger = useCallback(
(newValue: string, cursor: number) => {
const textBefore = newValue.slice(0, cursor);
const match = TRIGGER_RE.exec(textBefore);
if (match) {
triggerStartRef.current = match.index;
triggerCursorRef.current = cursor;
setState({ open: true, query: match[1] });
} else {
setState((s) => s.open ? { open: false, query: "" } : s);
}
},
[],
);
/**
* Called when frimousse fires onEmojiSelect.
* Replaces the ':query' text with the selected emoji.
*/
const handleEmojiSelect = useCallback(
(native: string) => {
const start = triggerStartRef.current;
const cursor = triggerCursorRef.current;
const newValue = value.slice(0, start) + native + " " +
value.slice(cursor);
onChange(newValue);
setState({ open: false, query: "" });
const newPos = start + native.length + 1; // +1 for trailing space
requestAnimationFrame(() => {
const el = textareaRef.current;
if (el) {
el.focus();
el.setSelectionRange(newPos, newPos);
}
});
},
[value, onChange, textareaRef],
);
const closeEmoji = useCallback(() => {
setState({ open: false, query: "" });
requestAnimationFrame(() => textareaRef.current?.focus());
}, [textareaRef]);
return {
emojiOpen: state.open,
emojiQuery: state.query,
detectEmojiTrigger,
handleEmojiSelect,
closeEmoji,
};
}