feat: enhance LookupInput with visual badges and flicker-free detection

- Add Badge component to display autodetection status with color coding
- Show green "Auto" badge when type is detected, red when unknown
- Prevent badge flickering during empty-to-non-empty input transitions
- Increase lock icon size from 14x14 to 16x16 pixels with blue color
- Track input state to manage smooth badge transitions
This commit is contained in:
2025-10-22 17:19:29 -05:00
parent 99b65363b4
commit 9e4c008680

View File

@@ -1,11 +1,11 @@
import { useForm, Controller } from "react-hook-form"; import { useForm, Controller } from "react-hook-form";
import type { FunctionComponent } from "react"; import type { FunctionComponent } from "react";
import { useState } from "react"; import { useState, useEffect } from "react";
import { onPromise, preventDefault } from "@/lib/utils"; import { onPromise, preventDefault } from "@/lib/utils";
import type { SimplifiedTargetType, SubmitProps, TargetType } from "@/rdap/schemas"; import type { SimplifiedTargetType, SubmitProps, TargetType } from "@/rdap/schemas";
import { TargetTypeEnum } from "@/rdap/schemas"; import { TargetTypeEnum } from "@/rdap/schemas";
import { MagnifyingGlassIcon, ReloadIcon, LockClosedIcon } from "@radix-ui/react-icons"; import { MagnifyingGlassIcon, ReloadIcon, LockClosedIcon } from "@radix-ui/react-icons";
import { TextField, Select, Flex, Checkbox, Text, IconButton } from "@radix-ui/themes"; import { TextField, Select, Flex, Checkbox, Text, IconButton, Badge } from "@radix-ui/themes";
import type { Maybe } from "true-myth"; import type { Maybe } from "true-myth";
import { placeholders } from "@/rdap/constants"; import { placeholders } from "@/rdap/constants";
@@ -85,6 +85,17 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
*/ */
const [selected, setSelected] = useState<SimplifiedTargetType | "auto">("auto"); const [selected, setSelected] = useState<SimplifiedTargetType | "auto">("auto");
/**
* Tracks the current input value to determine if the field is empty.
*/
const [inputValue, setInputValue] = useState<string>("");
/**
* Tracks whether we're waiting for type detection after transitioning from empty to non-empty input.
* Prevents badge from flickering during initial input.
*/
const [showingPlaceholder, setShowingPlaceholder] = useState<boolean>(false);
/** /**
* Retrieves the target type based on the provided value. * Retrieves the target type based on the provided value.
* @param value - The value to retrieve the target type for. * @param value - The value to retrieve the target type for.
@@ -102,6 +113,16 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
return result.success ? result.data : null; return result.success ? result.data : null;
} }
/**
* Clear the placeholder flag when type detection completes.
* This prevents badge flickering when transitioning from empty to non-empty input.
*/
useEffect(() => {
if (showingPlaceholder) {
setShowingPlaceholder(false);
}
}, [detectedType]);
return ( return (
<form <form
className="pb-2.5" className="pb-2.5"
@@ -120,9 +141,22 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
{...register("target", { {...register("target", {
required: true, required: true,
onChange: () => { onChange: () => {
const targetValue = getValues("target");
const oldIsEmpty = inputValue.trim() === "";
const newIsEmpty = targetValue.trim() === "";
// Transitioning from empty to non-empty - show placeholder until detection completes
if (oldIsEmpty && !newIsEmpty) {
setShowingPlaceholder(true);
} else if (newIsEmpty) {
// Input is now empty - clear placeholder
setShowingPlaceholder(false);
}
setInputValue(targetValue);
if (onChange != undefined) if (onChange != undefined)
void onChange({ void onChange({
target: getValues("target"), target: targetValue,
targetType: retrieveTargetType(null), targetType: retrieveTargetType(null),
}); });
}, },
@@ -177,14 +211,26 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
}} }}
> >
{selected == "auto" ? ( {selected == "auto" ? (
detectedType.isJust ? ( showingPlaceholder || inputValue.trim() === "" ? (
<Text>Auto ({targetShortNames[detectedType.value]})</Text>
) : (
objectNames["auto"] objectNames["auto"]
) : detectedType.isJust ? (
<Flex align="center" gap="2">
<Badge color="green">Auto</Badge>
{targetShortNames[detectedType.value]}
</Flex>
) : (
<Flex align="center" gap="2">
<Badge color="red">Auto</Badge>
Unknown
</Flex>
) )
) : ( ) : (
<Flex align="center" gap="2"> <Flex align="center" gap="2">
<LockClosedIcon width="14" height="14" /> <LockClosedIcon
width="16"
height="16"
style={{ color: "var(--blue-9)" }}
/>
{objectNames[selected]} {objectNames[selected]}
</Flex> </Flex>
)} )}