From 2002d8213c24bf7ec00901cbb466971759eae860 Mon Sep 17 00:00:00 2001
From: "Natalie B." <22222885+espeon@users.noreply.github.com>
Date: Tue, 2 Dec 2025 17:02:05 -0600
Subject: [PATCH] change to tab autocomplete
---
js/app/components/login/login-modal.tsx | 215 ++++++++++--------
js/app/components/login/login.tsx | 155 ++++++-------
js/components/src/components/ui/input.tsx | 8 +-
.../src/components/ui/primitives/input.tsx | 4 +-
4 files changed, 198 insertions(+), 184 deletions(-)
diff --git a/js/app/components/login/login-modal.tsx b/js/app/components/login/login-modal.tsx
index 253f7d436..61d7b8548 100644
--- a/js/app/components/login/login-modal.tsx
+++ b/js/app/components/login/login-modal.tsx
@@ -1,6 +1,6 @@
-import { Button, Text, useTheme, zero } from "@streamplace/components";
+import { Button, Input, Text, useTheme, zero } from "@streamplace/components";
import useActorTypeahead from "hooks/useActorTypeahead";
-import { Info, X } from "lucide-react-native";
+import { ArrowRightToLine, AtSign, Info, X } from "lucide-react-native";
import { useEffect, useState } from "react";
import {
Alert,
@@ -8,7 +8,6 @@ import {
Linking,
Modal,
Pressable,
- TextInput,
TouchableOpacity,
View,
} from "react-native";
@@ -26,28 +25,52 @@ export default function LoginModal({ visible, onClose }: LoginModalProps) {
const openLoginLink = useStore((state) => state.openLoginLink);
const loginState = useLogin();
const [handle, setHandle] = useState("");
- const [showSuggestions, setShowSuggestions] = useState(false);
- const { actors, loading: typeaheadLoading } = useActorTypeahead(handle);
+ const { actors } = useActorTypeahead(handle);
+
+ const filteredActors = actors.filter((actor) =>
+ actor.handle.startsWith(handle),
+ );
+
+ const suggestion =
+ filteredActors.length > 0 &&
+ handle.length >= 3 &&
+ filteredActors[0].handle.startsWith(handle)
+ ? filteredActors[0]
+ : null;
+
+ const completionText =
+ suggestion && suggestion.handle
+ ? suggestion.handle.slice(handle.length)
+ : null;
const submit = () => {
let clean = handle;
if (handle.startsWith("@")) clean = handle.slice(1);
- setShowSuggestions(false);
loginAction(clean, openLoginLink);
};
- const selectActor = (actorHandle: string) => {
- setHandle(actorHandle);
- setShowSuggestions(false);
+ const acceptSuggestion = () => {
+ if (suggestion) {
+ setHandle(suggestion.handle);
+ }
};
const onSignup = () => {
loginAction("https://bsky.social", openLoginLink);
};
- const onEnterPress = (e: any) => {
+ const onKeyPress = (e: any) => {
if (e.nativeEvent.key === "Enter") {
submit();
+ } else if (e.nativeEvent.key === "Tab" && completionText) {
+ e.preventDefault();
+ acceptSuggestion();
+ } else if (e.nativeEvent.key === "ArrowRight" && completionText) {
+ const input = e.target;
+ if (input.selectionStart === handle.length) {
+ e.preventDefault();
+ acceptSuggestion();
+ }
}
};
@@ -142,94 +165,102 @@ export default function LoginModal({ visible, onClose }: LoginModalProps) {
Handle
- {
- setHandle(
- text
- .toLowerCase()
- .replace(/[\u202A\u202C\u200E\u200F\u2066-\u2069]/g, "")
- .trim(),
- );
- setShowSuggestions(true);
- }}
- onFocus={() => setShowSuggestions(true)}
- onBlur={() => {
- setTimeout(() => setShowSuggestions(false), 200);
- }}
- style={[
- {
- backgroundColor: "#1a1a1a",
- borderWidth: 1,
- borderColor: "#333",
- borderRadius: 8,
- padding: 12,
- color: "white",
- },
- ]}
- onSubmitEditing={onEnterPress}
- autoCapitalize="none"
- autoCorrect={false}
- keyboardType="url"
- placeholderTextColor="#666"
- />
- {showSuggestions && actors.length > 0 && (
-
- {actors.map((actor) => (
- selectActor(actor.handle)}
+
+ {completionText && (
+
+
- {actor.avatar && (
-
- )}
-
- {actor.displayName && (
-
- {actor.displayName}
-
- )}
-
- @{actor.handle}
-
-
-
- ))}
+
+ {handle}
+
+ {completionText}
+
+
+
+ )}
+
+ {suggestion?.avatar ? (
+
+ ) : (
+
+
+
+ )}
- )}
+
+ setHandle(
+ text
+ .toLowerCase()
+ .replace(/[\u202A\u202C\u200E\u200F\u2066-\u2069]/g, "")
+ .trim(),
+ )
+ }
+ onKeyPress={onKeyPress}
+ autoCapitalize="none"
+ autoCorrect={false}
+ keyboardType="url"
+ placeholderTextColor="#666"
+ containerStyle={{
+ marginLeft: 28 + 8,
+ }}
+ />
+
0 &&
+ handle.length >= 3 &&
+ actors[0].handle.startsWith(handle)
+ ? actors[0].handle
+ : null;
+
+ const completionText = suggestion ? suggestion.slice(handle.length) : null;
// null: no return route, undefined: hasn't checked yet
const [localReturnRoute, setLocalReturnRoute] = useState<
| {
@@ -68,20 +74,31 @@ export default function Login() {
const submit = () => {
let clean = handle;
if (handle.startsWith("@")) clean = handle.slice(1);
- setShowSuggestions(false);
loginAction(clean, openLoginLink);
};
- const selectActor = (actorHandle: string) => {
- setHandle(actorHandle);
- setShowSuggestions(false);
+ const acceptSuggestion = () => {
+ if (suggestion) {
+ setHandle(suggestion);
+ }
};
+
const onSignup = () => {
loginAction("https://bsky.social", openLoginLink);
};
- const onEnterPress = (e: any) => {
+
+ const onKeyPress = (e: any) => {
if (e.nativeEvent.key === "Enter") {
submit();
+ } else if (e.nativeEvent.key === "Tab" && completionText) {
+ e.preventDefault();
+ acceptSuggestion();
+ } else if (e.nativeEvent.key === "ArrowRight" && completionText) {
+ const input = e.target;
+ if (input.selectionStart === handle.length) {
+ e.preventDefault();
+ acceptSuggestion();
+ }
}
};
@@ -178,95 +195,53 @@ export default function Login() {
Handle
- {
- setHandle(
- text
- .toLowerCase()
- // copying from bsky.app often includes some RTL/LTR characters
- .replace(/[\u202A\u202C\u200E\u200F\u2066-\u2069]/g, "")
- .trim(),
- );
- setShowSuggestions(true);
- }}
- onFocus={() => setShowSuggestions(true)}
- onBlur={() => {
- setTimeout(() => setShowSuggestions(false), 200);
- }}
- style={[
- {
- backgroundColor: "#1a1a1a",
- borderWidth: 1,
- borderColor: "#333",
- borderRadius: 8,
- padding: 12,
- color: "white",
- },
- ]}
- onSubmitEditing={onEnterPress}
- autoCapitalize="none"
- autoCorrect={false}
- keyboardType="url"
- placeholderTextColor="#666"
- />
- {showSuggestions && actors.length > 0 && (
-
+ {completionText && (
+
+ {handle}
+ {completionText}
+
+ )}
+
+ setHandle(
+ text
+ .toLowerCase()
+ .replace(/[\u202A\u202C\u200E\u200F\u2066-\u2069]/g, "")
+ .trim(),
+ )
+ }
+ onKeyPress={onKeyPress}
style={[
{
- position: "absolute",
- top: "100%",
- left: 0,
- right: 0,
backgroundColor: "#1a1a1a",
borderWidth: 1,
borderColor: "#333",
borderRadius: 8,
- marginTop: 4,
- maxHeight: 200,
- zIndex: 1000,
+ padding: 12,
+ color: "white",
+ position: "relative",
+ zIndex: 2,
},
]}
- >
- {actors.map((actor) => (
- selectActor(actor.handle)}
- style={[
- {
- flexDirection: "row",
- alignItems: "center",
- padding: 12,
- borderBottomWidth: 1,
- borderBottomColor: "#222",
- },
- ]}
- >
- {actor.avatar && (
-
- )}
-
- {actor.displayName && (
-
- {actor.displayName}
-
- )}
-
- @{actor.handle}
-
-
-
- ))}
-
- )}
+ autoCapitalize="none"
+ autoCorrect={false}
+ keyboardType="url"
+ placeholderTextColor="#666"
+ />
+
(
error={!!error}
onFocus={handleFocus}
onBlur={handleBlur}
- style={[containerStyles, textStyles, containerStyle, inputStyle]}
+ style={[
+ containerStyles,
+ textStyles,
+ containerStyle,
+ inputStyle,
+ { outline: "none" },
+ ]}
placeholderTextColor={
disabled ? theme.colors.textDisabled : theme.colors.textMuted
}
diff --git a/js/components/src/components/ui/primitives/input.tsx b/js/components/src/components/ui/primitives/input.tsx
index 8d8db71d2..a1ba6f880 100644
--- a/js/components/src/components/ui/primitives/input.tsx
+++ b/js/components/src/components/ui/primitives/input.tsx
@@ -16,6 +16,7 @@ import {
View,
ViewProps,
} from "react-native";
+import { tokens } from "../../../ui";
// Base input primitive interface
export interface InputPrimitiveProps extends Omit {
@@ -329,7 +330,8 @@ const primitiveStyles = StyleSheet.create({
borderWidth: 1,
borderColor: "#d1d5db",
borderRadius: 8,
- backgroundColor: "white",
+ boxShadow: "none",
+ fontFamily: tokens.fontFamilies.regular,
...Platform.select({
ios: {
paddingVertical: 12,
--
2.51.2