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