feat: add emoji picker
This commit is contained in:
parent
963a37292d
commit
1d0b5ca55e
6 changed files with 122 additions and 19 deletions
|
|
@ -8,7 +8,6 @@
|
|||
"name": "Alexander Daichendt",
|
||||
"url": "https://daichendt.one"
|
||||
},
|
||||
"packageManager": "pnpm@10.3.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
|
|
@ -19,6 +18,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.0.6",
|
||||
"emoji-picker-react": "^4.12.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.1.5",
|
||||
|
|
|
|||
19
client/pnpm-lock.yaml
generated
19
client/pnpm-lock.yaml
generated
|
|
@ -11,6 +11,9 @@ importers:
|
|||
'@tailwindcss/vite':
|
||||
specifier: ^4.0.6
|
||||
version: 4.0.6(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1))
|
||||
emoji-picker-react:
|
||||
specifier: ^4.12.0
|
||||
version: 4.12.0(react@19.0.0)
|
||||
react:
|
||||
specifier: ^19.0.0
|
||||
version: 19.0.0
|
||||
|
|
@ -920,6 +923,12 @@ packages:
|
|||
electron-to-chromium@1.5.97:
|
||||
resolution: {integrity: sha512-HKLtaH02augM7ZOdYRuO19rWDeY+QSJ1VxnXFa/XDFLf07HvM90pALIJFgrO+UVaajI3+aJMMpojoUTLZyQ7JQ==}
|
||||
|
||||
emoji-picker-react@4.12.0:
|
||||
resolution: {integrity: sha512-q2c8UcZH0eRIMj41bj0k1akTjk69tsu+E7EzkW7giN66iltF6H9LQvQvw6ugscsxdC+1lmt3WZpQkkY65J95tg==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
react: '>=16'
|
||||
|
||||
enhanced-resolve@5.18.1:
|
||||
resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
|
@ -1032,6 +1041,9 @@ packages:
|
|||
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
flairup@1.0.0:
|
||||
resolution: {integrity: sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA==}
|
||||
|
||||
flat-cache@4.0.1:
|
||||
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
|
||||
engines: {node: '>=16'}
|
||||
|
|
@ -2503,6 +2515,11 @@ snapshots:
|
|||
|
||||
electron-to-chromium@1.5.97: {}
|
||||
|
||||
emoji-picker-react@4.12.0(react@19.0.0):
|
||||
dependencies:
|
||||
flairup: 1.0.0
|
||||
react: 19.0.0
|
||||
|
||||
enhanced-resolve@5.18.1:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
|
|
@ -2658,6 +2675,8 @@ snapshots:
|
|||
locate-path: 6.0.0
|
||||
path-exists: 4.0.0
|
||||
|
||||
flairup@1.0.0: {}
|
||||
|
||||
flat-cache@4.0.1:
|
||||
dependencies:
|
||||
flatted: 3.3.2
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useState } from "react";
|
||||
import { ClientChatMessage, ClientMessage } from "../../../shared";
|
||||
import EmojiPicker from "./EmojiPicker";
|
||||
|
||||
interface ChatInputProps {
|
||||
sendMessage: (message: ClientMessage) => void;
|
||||
|
|
@ -23,20 +24,36 @@ function ChatInput({ sendMessage }: ChatInputProps) {
|
|||
setMessage("");
|
||||
}
|
||||
|
||||
const handleEmojiSelect = (emoji: string) => {
|
||||
setMessage((prevMessage) => prevMessage + emoji);
|
||||
};
|
||||
|
||||
// this is required as otherwise pressing enter will open the emoji picker and not submit the message
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleSubmit(e);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="mt-4">
|
||||
<label htmlFor="chat-input" className="sr-only">
|
||||
Type a message
|
||||
</label>
|
||||
<input
|
||||
id="chat-input"
|
||||
type="text"
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
placeholder="Type a message" // this message is more descriptive than a simple "Message", therefore better for accessibility
|
||||
aria-label="Chat message input"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor="chat-input" className="sr-only">
|
||||
Type a message
|
||||
</label>
|
||||
<input
|
||||
id="chat-input"
|
||||
type="text"
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Type a message"
|
||||
aria-label="Chat message input"
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
|
||||
<EmojiPicker onEmojiClick={handleEmojiSelect} />
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
68
client/src/components/EmojiPicker.tsx
Normal file
68
client/src/components/EmojiPicker.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { EmojiClickData, EmojiStyle } from "emoji-picker-react";
|
||||
import EmojiPickerComponent from "emoji-picker-react";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
|
||||
interface EmojiPickerProps {
|
||||
onEmojiClick: (emoji: string) => void;
|
||||
}
|
||||
|
||||
function EmojiPicker({ onEmojiClick }: EmojiPickerProps) {
|
||||
const [showPicker, setShowPicker] = useState(false);
|
||||
const pickerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController(); // cool usage of AbortController to handle listener cleanup
|
||||
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (
|
||||
pickerRef.current &&
|
||||
!pickerRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setShowPicker(false);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
return () => controller.abort();
|
||||
}, []);
|
||||
|
||||
const handleEmojiClick = (emojiData: EmojiClickData) => {
|
||||
onEmojiClick(emojiData.emoji);
|
||||
setShowPicker(false);
|
||||
};
|
||||
|
||||
const handleToggleEmojiPicker = (
|
||||
event: React.MouseEvent<HTMLButtonElement>,
|
||||
) => {
|
||||
// required to prevent default behavior of button click, which would trigger a form submit
|
||||
event.preventDefault();
|
||||
setShowPicker(!showPicker);
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={pickerRef} className="relative">
|
||||
<button
|
||||
role="button"
|
||||
onClick={handleToggleEmojiPicker}
|
||||
className="p-2 text-gray-500 hover:text-gray-700 focus:outline-none"
|
||||
aria-label="Open emoji picker"
|
||||
>
|
||||
😊
|
||||
</button>
|
||||
{showPicker && (
|
||||
<div className="absolute bottom-12 right-0 z-50">
|
||||
<EmojiPickerComponent
|
||||
lazyLoadEmojis
|
||||
emojiStyle={EmojiStyle.NATIVE} // uses native emoji style of the operating system, otherwise the displayed emoji will differ from what is shown in the picker
|
||||
onEmojiClick={handleEmojiClick}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EmojiPicker;
|
||||
Loading…
Add table
Add a link
Reference in a new issue