fix(room): update keyboard shortcuts — Ctrl+Enter sends, Shift+Enter newlines, Enter only for mention select
- Ctrl+Enter: send message (was plain Enter) - Shift+Enter: insert newline (textarea default, passes through) - Enter alone: only triggers mention selection when popover is open, otherwise does nothing - Update MentionPopover footer/header hints to reflect new shortcuts
This commit is contained in:
parent
9b966789fd
commit
14bcc04991
@ -61,6 +61,8 @@ interface MentionPopoverProps {
|
|||||||
onSelect: (newValue: string, newCursorPosition: number) => void;
|
onSelect: (newValue: string, newCursorPosition: number) => void;
|
||||||
textareaRef: React.RefObject<HTMLTextAreaElement | null>;
|
textareaRef: React.RefObject<HTMLTextAreaElement | null>;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
|
/** Mutable ref: write the confirm fn here so ChatInputArea can call it on Enter */
|
||||||
|
onMentionConfirmRef?: React.MutableRefObject<() => void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Category configuration with icons and colors
|
// Category configuration with icons and colors
|
||||||
@ -372,6 +374,7 @@ export function MentionPopover({
|
|||||||
onSelect,
|
onSelect,
|
||||||
textareaRef,
|
textareaRef,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
|
onMentionConfirmRef,
|
||||||
}: MentionPopoverProps) {
|
}: MentionPopoverProps) {
|
||||||
const [position, setPosition] = useState<PopoverPosition>({ top: 0, left: 0 });
|
const [position, setPosition] = useState<PopoverPosition>({ top: 0, left: 0 });
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
@ -521,6 +524,14 @@ export function MentionPopover({
|
|||||||
|
|
||||||
handleSelectRef.current = handleSelect;
|
handleSelectRef.current = handleSelect;
|
||||||
|
|
||||||
|
// Register confirm fn so ChatInputArea can call it on Enter
|
||||||
|
if (onMentionConfirmRef) {
|
||||||
|
onMentionConfirmRef.current = () => {
|
||||||
|
const item = visibleSuggestionsRef.current[selectedIndexRef.current];
|
||||||
|
if (item) handleSelectRef.current(item);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Position calculation
|
// Position calculation
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mentionState || !textareaRef.current) return;
|
if (!mentionState || !textareaRef.current) return;
|
||||||
@ -723,6 +734,11 @@ export function MentionPopover({
|
|||||||
↑↓
|
↑↓
|
||||||
</kbd>
|
</kbd>
|
||||||
<span>navigate</span>
|
<span>navigate</span>
|
||||||
|
<span className="text-muted-foreground/50">|</span>
|
||||||
|
<kbd className="px-1.5 py-0.5 rounded bg-muted border border-border/50 font-mono">
|
||||||
|
↵
|
||||||
|
</kbd>
|
||||||
|
<span>select</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -779,7 +795,7 @@ export function MentionPopover({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<kbd className="px-1.5 py-0.5 rounded bg-muted border border-border/50 font-mono text-[10px]">
|
<kbd className="px-1.5 py-0.5 rounded bg-muted border border-border/50 font-mono text-[10px]">
|
||||||
Enter
|
Ctrl+Enter
|
||||||
</kbd>
|
</kbd>
|
||||||
<span className="text-[10px] text-muted-foreground">to insert</span>
|
<span className="text-[10px] text-muted-foreground">to insert</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -48,6 +48,8 @@ interface ChatInputAreaProps {
|
|||||||
draft: string;
|
draft: string;
|
||||||
onDraftChange: (content: string) => void;
|
onDraftChange: (content: string) => void;
|
||||||
onClearDraft: () => void;
|
onClearDraft: () => void;
|
||||||
|
/** Called when Enter is pressed with mention popover open — parent triggers mention selection */
|
||||||
|
onMentionConfirm?: () => void;
|
||||||
ref?: React.Ref<ChatInputAreaHandle>;
|
ref?: React.Ref<ChatInputAreaHandle>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,6 +67,7 @@ const ChatInputArea = memo(function ChatInputArea({
|
|||||||
draft,
|
draft,
|
||||||
onDraftChange,
|
onDraftChange,
|
||||||
onClearDraft,
|
onClearDraft,
|
||||||
|
onMentionConfirm,
|
||||||
ref,
|
ref,
|
||||||
}: ChatInputAreaProps) {
|
}: ChatInputAreaProps) {
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
@ -88,7 +91,6 @@ const ChatInputArea = memo(function ChatInputArea({
|
|||||||
if (!textareaRef.current) return;
|
if (!textareaRef.current) return;
|
||||||
const value = textareaRef.current.value;
|
const value = textareaRef.current.value;
|
||||||
const cursorPos = textareaRef.current.selectionStart;
|
const cursorPos = textareaRef.current.selectionStart;
|
||||||
// Build new HTML mention: <mention type="user" id="uuid">label</mention>
|
|
||||||
const escapedLabel = label.replace(/</g, '<').replace(/>/g, '>');
|
const escapedLabel = label.replace(/</g, '<').replace(/>/g, '>');
|
||||||
const escapedId = id.replace(/"/g, '"');
|
const escapedId = id.replace(/"/g, '"');
|
||||||
const mentionText = `<mention type="user" id="${escapedId}">${escapedLabel}</mention> `;
|
const mentionText = `<mention type="user" id="${escapedId}">${escapedLabel}</mention> `;
|
||||||
@ -122,12 +124,18 @@ const ChatInputArea = memo(function ChatInputArea({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (showMentionPopover && MENTION_POPOVER_KEYS.includes(e.key)) {
|
if (showMentionPopover && MENTION_POPOVER_KEYS.includes(e.key) && !e.ctrlKey && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if ((e.key === 'Enter' || e.key === 'Tab') && onMentionConfirm) {
|
||||||
|
onMentionConfirm();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
// Shift+Enter → let textarea naturally insert newline (pass through)
|
||||||
|
|
||||||
|
// Ctrl+Enter → send message
|
||||||
|
if (e.key === 'Enter' && e.ctrlKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const content = e.currentTarget.value.trim();
|
const content = e.currentTarget.value.trim();
|
||||||
if (content && !isSending) {
|
if (content && !isSending) {
|
||||||
@ -135,6 +143,12 @@ const ChatInputArea = memo(function ChatInputArea({
|
|||||||
onClearDraft();
|
onClearDraft();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Plain Enter (no modifiers) → only trigger mention select; otherwise do nothing
|
||||||
|
if (e.key === 'Enter' && !e.ctrlKey && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
// Do nothing — Shift+Enter falls through to let textarea insert newline
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -190,6 +204,7 @@ const ChatInputArea = memo(function ChatInputArea({
|
|||||||
onSelect={handleMentionSelect}
|
onSelect={handleMentionSelect}
|
||||||
textareaRef={textareaRef}
|
textareaRef={textareaRef}
|
||||||
onOpenChange={setShowMentionPopover}
|
onOpenChange={setShowMentionPopover}
|
||||||
|
onMentionConfirmRef={mentionConfirmRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -268,6 +283,8 @@ export function RoomChatPanel({ room, isAdmin, onClose, onDelete }: RoomChatPane
|
|||||||
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
const chatInputRef = useRef<ChatInputAreaHandle>(null);
|
const chatInputRef = useRef<ChatInputAreaHandle>(null);
|
||||||
|
/** Shared ref: MentionPopover writes the confirm fn here; ChatInputArea reads and calls it */
|
||||||
|
const mentionConfirmRef = useRef<() => void>(() => {});
|
||||||
|
|
||||||
const [replyingTo, setReplyingTo] = useState<MessageWithMeta | null>(null);
|
const [replyingTo, setReplyingTo] = useState<MessageWithMeta | null>(null);
|
||||||
const [editingMessage, setEditingMessage] = useState<MessageWithMeta | null>(null);
|
const [editingMessage, setEditingMessage] = useState<MessageWithMeta | null>(null);
|
||||||
@ -559,6 +576,7 @@ export function RoomChatPanel({ room, isAdmin, onClose, onDelete }: RoomChatPane
|
|||||||
draft={draft}
|
draft={draft}
|
||||||
onDraftChange={setDraft}
|
onDraftChange={setDraft}
|
||||||
onClearDraft={clearDraft}
|
onClearDraft={clearDraft}
|
||||||
|
onMentionConfirm={() => mentionConfirmRef.current()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user