feat: create tests, more integration testing, fix ID tests, use UUID (almost) everywhere
ref: N25B-412
This commit is contained in:
@@ -1,13 +1,23 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useState, useRef } from "react";
|
||||
import styles from './GestureValueEditor.module.css'
|
||||
|
||||
/**
|
||||
* Props for the GestureValueEditor component.
|
||||
* - value: current gesture value (controlled by parent)
|
||||
* - setValue: callback to update the gesture value in parent state
|
||||
* - placeholder: optional placeholder text for the input field
|
||||
*/
|
||||
type GestureValueEditorProps = {
|
||||
value: string;
|
||||
setValue: (value: string) => void;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
// Define your gesture tags here
|
||||
/**
|
||||
* List of high-level gesture "tags".
|
||||
* These are human-readable categories or semantic labels.
|
||||
* In a real app, these would likely be loaded from an external source.
|
||||
*/
|
||||
const GESTURE_TAGS = ["above", "affirmative", "afford", "agitated", "all", "allright", "alright", "any",
|
||||
"assuage", "attemper", "back", "bashful", "beg", "beseech", "blank",
|
||||
"body language", "bored", "bow", "but", "call", "calm", "choose", "choice", "cloud",
|
||||
@@ -23,6 +33,11 @@ const GESTURE_TAGS = ["above", "affirmative", "afford", "agitated", "all", "allr
|
||||
"think", "timid", "top", "unless", "up", "upstairs", "void", "warm", "winner", "yeah",
|
||||
"yes", "yoo-hoo", "you", "your", "zero", "zestful"];
|
||||
|
||||
/**
|
||||
* List of concrete gesture animation paths.
|
||||
* These represent specific animation assets and are used in "single" mode
|
||||
* with autocomplete-style selection, also would be loaded from an external source.
|
||||
*/
|
||||
const GESTURE_SINGLES = [
|
||||
"animations/Stand/BodyTalk/Listening/Listening_1",
|
||||
"animations/Stand/BodyTalk/Listening/Listening_2",
|
||||
@@ -421,50 +436,62 @@ const GESTURE_SINGLES = [
|
||||
"animations/Stand/Waiting/Zombie_1"]
|
||||
|
||||
|
||||
/**
|
||||
* Returns a gesture value editor component.
|
||||
* @returns JSX.Element
|
||||
*/
|
||||
export default function GestureValueEditor({
|
||||
value,
|
||||
setValue,
|
||||
placeholder = "Gesture name",
|
||||
}: GestureValueEditorProps) {
|
||||
|
||||
/** Input mode: semantic tag vs concrete animation path */
|
||||
const [mode, setMode] = useState<"single" | "tag">("tag");
|
||||
|
||||
/** Raw text value for single-gesture input */
|
||||
const [customValue, setCustomValue] = useState("");
|
||||
|
||||
/** Autocomplete dropdown state */
|
||||
const [showSuggestions, setShowSuggestions] = useState(true);
|
||||
const [filteredSuggestions, setFilteredSuggestions] = useState<string[]>([]);
|
||||
|
||||
/** Reserved for future click-outside / positioning logic */
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
/** Switch between tag and single input modes */
|
||||
const handleModeChange = (newMode: "single" | "tag") => {
|
||||
setMode(newMode);
|
||||
|
||||
if (newMode === "single") {
|
||||
// When switching to single, use custom value or existing value
|
||||
setValue(customValue || value);
|
||||
setFilteredSuggestions(GESTURE_SINGLES);
|
||||
setShowSuggestions(true);
|
||||
} else {
|
||||
// When switching to tag, clear value if not a valid tag
|
||||
const isCurrentValueTag = GESTURE_TAGS.some(tag =>
|
||||
tag.toLowerCase() === value.toLowerCase()
|
||||
// Clear value if it does not match a valid tag
|
||||
const isValidTag = GESTURE_TAGS.some(
|
||||
tag => tag.toLowerCase() === value.toLowerCase()
|
||||
);
|
||||
if (!isCurrentValueTag) {
|
||||
setValue("");
|
||||
}
|
||||
if (!isValidTag) setValue("");
|
||||
setShowSuggestions(false);
|
||||
}
|
||||
};
|
||||
|
||||
/** Select a semantic gesture tag */
|
||||
const handleTagSelect = (tag: string) => {
|
||||
setValue(tag);
|
||||
};
|
||||
|
||||
/** Update single-gesture input and filter suggestions */
|
||||
const handleCustomChange = (newValue: string) => {
|
||||
setCustomValue(newValue);
|
||||
setValue(newValue);
|
||||
|
||||
// Filter suggestions based on input
|
||||
|
||||
if (newValue.trim() === "") {
|
||||
setShowSuggestions(true)
|
||||
setFilteredSuggestions(GESTURE_SINGLES);
|
||||
setShowSuggestions(true);
|
||||
} else {
|
||||
const filtered = GESTURE_SINGLES.filter(single =>
|
||||
const filtered = GESTURE_SINGLES.filter(single =>
|
||||
single.toLowerCase().includes(newValue.toLowerCase())
|
||||
);
|
||||
setFilteredSuggestions(filtered);
|
||||
@@ -472,30 +499,32 @@ export default function GestureValueEditor({
|
||||
}
|
||||
};
|
||||
|
||||
/** Commit autocomplete selection */
|
||||
const handleSuggestionSelect = (suggestion: string) => {
|
||||
setCustomValue(suggestion);
|
||||
setValue(suggestion);
|
||||
setShowSuggestions(false);
|
||||
};
|
||||
|
||||
/** Refresh suggestions on refocus */
|
||||
const handleInputFocus = () => {
|
||||
if (customValue.trim() !== "") {
|
||||
const filtered = GESTURE_SINGLES.filter(tag =>
|
||||
tag.toLowerCase().includes(customValue.toLowerCase())
|
||||
);
|
||||
setFilteredSuggestions(filtered);
|
||||
setShowSuggestions(filtered.length > 0);
|
||||
}
|
||||
if (!customValue.trim()) return;
|
||||
|
||||
const filtered = GESTURE_SINGLES.filter(single =>
|
||||
single.toLowerCase().includes(customValue.toLowerCase())
|
||||
);
|
||||
setFilteredSuggestions(filtered);
|
||||
setShowSuggestions(filtered.length > 0);
|
||||
};
|
||||
|
||||
const handleInputBlur = (_e: React.FocusEvent) => {
|
||||
// Delay hiding suggestions to allow clicking on them
|
||||
/** Exists to allow delayed blur handling if needed */
|
||||
const handleInputBlur = (_e: React.FocusEvent) => {};
|
||||
|
||||
};
|
||||
|
||||
/** Build the JSX component */
|
||||
return (
|
||||
<div className={styles.gestureEditor} ref={containerRef}>
|
||||
{/* Mode selector */}
|
||||
{/* Mode toggle */}
|
||||
<div className={styles.modeSelector}>
|
||||
<label className={styles.modeLabel}>Input Mode:</label>
|
||||
<div className={styles.toggleContainer}>
|
||||
@@ -516,8 +545,7 @@ export default function GestureValueEditor({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Value editor based on mode */}
|
||||
<div className={styles.valueEditor}>
|
||||
<div className={styles.valueEditor} data-testid={"valueEditorTestID"}>
|
||||
{mode === "single" ? (
|
||||
<div className={styles.autocompleteContainer}>
|
||||
{showSuggestions && (
|
||||
@@ -527,7 +555,7 @@ export default function GestureValueEditor({
|
||||
key={suggestion}
|
||||
className={styles.suggestionItem}
|
||||
onClick={() => handleSuggestionSelect(suggestion)}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onMouseDown={(e) => e.preventDefault()} // prevent blur before click
|
||||
>
|
||||
{suggestion}
|
||||
</div>
|
||||
@@ -551,14 +579,14 @@ export default function GestureValueEditor({
|
||||
value={value}
|
||||
onChange={(e) => handleTagSelect(e.target.value)}
|
||||
className={styles.tagSelect}
|
||||
data-testid={"tagSelectorTestID"}
|
||||
>
|
||||
<option value="">Select a gesture tag...</option>
|
||||
<option value="" >Select a gesture tag...</option>
|
||||
{GESTURE_TAGS.map((tag) => (
|
||||
<option key={tag} value={tag}>
|
||||
{tag}
|
||||
</option>
|
||||
<option key={tag} value={tag}>{tag}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<div className={styles.tagList}>
|
||||
{GESTURE_TAGS.map((tag) => (
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user