From 6ec259a0e71174651bae95d4628138bf6fd68742 Mon Sep 17 00:00:00 2001
From: kj_sh604
Date: Sun, 15 Mar 2026 16:19:35 -0400
Subject: refactor: packages/

---
 packages/excalidraw/components/TextField.tsx | 112 +++++++++++++++++++++++++++
 1 file changed, 112 insertions(+)
 create mode 100644 packages/excalidraw/components/TextField.tsx

(limited to 'packages/excalidraw/components/TextField.tsx')

diff --git a/packages/excalidraw/components/TextField.tsx b/packages/excalidraw/components/TextField.tsx
new file mode 100644
index 0000000..c5bdc82
--- /dev/null
+++ b/packages/excalidraw/components/TextField.tsx
@@ -0,0 +1,112 @@
+import type { KeyboardEvent } from "react";
+import {
+  forwardRef,
+  useRef,
+  useImperativeHandle,
+  useLayoutEffect,
+  useState,
+} from "react";
+import clsx from "clsx";
+
+import "./TextField.scss";
+import { Button } from "./Button";
+import { eyeIcon, eyeClosedIcon } from "./icons";
+
+type TextFieldProps = {
+  onChange?: (value: string) => void;
+  onClick?: () => void;
+  onKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => void;
+
+  readonly?: boolean;
+  fullWidth?: boolean;
+  selectOnRender?: boolean;
+
+  icon?: React.ReactNode;
+  label?: string;
+  className?: string;
+  placeholder?: string;
+  isRedacted?: boolean;
+} & ({ value: string } | { defaultValue: string });
+
+export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
+  (
+    {
+      onChange,
+      label,
+      fullWidth,
+      placeholder,
+      readonly,
+      selectOnRender,
+      onKeyDown,
+      isRedacted = false,
+      icon,
+      className,
+      ...rest
+    },
+    ref,
+  ) => {
+    const innerRef = useRef<HTMLInputElement | null>(null);
+
+    useImperativeHandle(ref, () => innerRef.current!);
+
+    useLayoutEffect(() => {
+      if (selectOnRender) {
+        // focusing first is needed because vitest/jsdom
+        innerRef.current?.focus();
+        innerRef.current?.select();
+      }
+    }, [selectOnRender]);
+
+    const [isTemporarilyUnredacted, setIsTemporarilyUnredacted] =
+      useState<boolean>(false);
+
+    return (
+      <div
+        className={clsx("ExcTextField", className, {
+          "ExcTextField--fullWidth": fullWidth,
+          "ExcTextField--hasIcon": !!icon,
+        })}
+        onClick={() => {
+          innerRef.current?.focus();
+        }}
+      >
+        {icon}
+        {label && <div className="ExcTextField__label">{label}</div>}
+        <div
+          className={clsx("ExcTextField__input", {
+            "ExcTextField__input--readonly": readonly,
+          })}
+        >
+          <input
+            className={clsx({
+              "is-redacted":
+                "value" in rest &&
+                rest.value &&
+                isRedacted &&
+                !isTemporarilyUnredacted,
+            })}
+            readOnly={readonly}
+            value={"value" in rest ? rest.value : undefined}
+            defaultValue={
+              "defaultValue" in rest ? rest.defaultValue : undefined
+            }
+            placeholder={placeholder}
+            ref={innerRef}
+            onChange={(event) => onChange?.(event.target.value)}
+            onKeyDown={onKeyDown}
+          />
+          {isRedacted && (
+            <Button
+              onSelect={() =>
+                setIsTemporarilyUnredacted(!isTemporarilyUnredacted)
+              }
+              style={{ border: 0, userSelect: "none" }}
+            >
+              {isTemporarilyUnredacted ? eyeClosedIcon : eyeIcon}
+            </Button>
+          )}
+        </div>
+      </div>
+    );
+  },
+);
-- 
cgit v1.2.3

