refactor: simplify PasswordInput component and standardize usage

Removes unnecessary props and internal state management from PasswordInput component to make it more focused and reusable. Wraps all PasswordInput instances in relative positioned Views for consistent layout behavior.

Updates package.json to use caret version for @expo/vector-icons dependency for better version flexibility.
This commit is contained in:
Uruk
2025-09-01 19:18:54 +02:00
parent 2f2e5a2730
commit 4dce87dfd3
6 changed files with 109 additions and 126 deletions

View File

@@ -277,20 +277,19 @@ const Login: React.FC = () => {
/> />
{/* Password */} {/* Password */}
<PasswordInput <View className='relative'>
value={credentials.password} <PasswordInput
onChangeText={(text: string) => value={credentials.password}
setCredentials({ ...credentials, password: text }) onChangeText={(text: string) =>
} setCredentials({ ...credentials, password: text })
onSubmitEditing={handleLogin} }
placeholder={t("login.password_placeholder")} placeholder={t("login.password_placeholder")}
className='mb-4' showPassword={showPassword}
showPassword={showPassword} onShowPasswordChange={setShowPassword}
onShowPasswordChange={setShowPassword} topPosition='4'
topPosition='4' layout='tv'
testID='tv-password-input' />
accessibilityLabel={t("login.password_placeholder")} </View>
/>
<View className='mt-4'> <View className='mt-4'>
<Button onPress={handleLogin}>{t("login.login_button")}</Button> <Button onPress={handleLogin}>{t("login.login_button")}</Button>
@@ -403,20 +402,22 @@ const Login: React.FC = () => {
textContentType='oneTimeCode' textContentType='oneTimeCode'
clearButtonMode='while-editing' clearButtonMode='while-editing'
maxLength={500} maxLength={500}
extraClassName='mb-4'
/> />
<PasswordInput <View className='relative mb-0.5'>
value={credentials.password} <PasswordInput
onChangeText={(text) => value={credentials.password}
setCredentials({ ...credentials, password: text }) onChangeText={(text) =>
} setCredentials({ ...credentials, password: text })
placeholder={t("login.password_placeholder")} }
showPassword={showPassword} placeholder={t("login.password_placeholder")}
onShowPasswordChange={setShowPassword} showPassword={showPassword}
topPosition='3.5' onShowPasswordChange={setShowPassword}
testID='mobile-password-input' topPosition='10'
accessibilityLabel={t("login.password_placeholder")} layout='mobile'
/> />
</View>
<View className='flex flex-row items-center justify-between'> <View className='flex flex-row items-center justify-between'>
<Button <Button
onPress={handleLogin} onPress={handleLogin}
@@ -427,7 +428,7 @@ const Login: React.FC = () => {
</Button> </Button>
<TouchableOpacity <TouchableOpacity
onPress={handleQuickConnect} onPress={handleQuickConnect}
className='p-2 bg-neutral-900 rounded-xl h-12 w-12 flex items-center justify-center' className='p-3 bg-neutral-900 rounded-xl h-13 w-13 flex items-center justify-center'
> >
<MaterialCommunityIcons <MaterialCommunityIcons
name='cellphone-lock' name='cellphone-lock'

View File

@@ -7,7 +7,7 @@
"@bottom-tabs/react-navigation": "^0.9.2", "@bottom-tabs/react-navigation": "^0.9.2",
"@expo/metro-runtime": "~5.0.4", "@expo/metro-runtime": "~5.0.4",
"@expo/react-native-action-sheet": "^4.1.1", "@expo/react-native-action-sheet": "^4.1.1",
"@expo/vector-icons": "15.0.2", "@expo/vector-icons": "^15.0.2",
"@gorhom/bottom-sheet": "^5.1.0", "@gorhom/bottom-sheet": "^5.1.0",
"@jellyfin/sdk": "^0.11.0", "@jellyfin/sdk": "^0.11.0",
"@kesha-antonov/react-native-background-downloader": "^3.2.6", "@kesha-antonov/react-native-background-downloader": "^3.2.6",

View File

@@ -1,104 +1,90 @@
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import type React from "react"; import type React from "react";
import { useState } from "react"; import { TouchableOpacity } from "react-native";
import { useTranslation } from "react-i18next";
import { TouchableOpacity, View } from "react-native";
import { Input } from "./common/Input"; import { Input } from "./common/Input";
interface PasswordInputProps { // Discriminated union for password visibility control
type PasswordVisibilityControlled = {
value?: string; value?: string;
onChangeText: (text: string) => void; onChangeText: (text: string) => void;
onSubmitEditing?: () => void;
placeholder: string; placeholder: string;
className?: string; showPassword: boolean;
testID?: string; onShowPasswordChange: (show: boolean) => void;
accessibilityLabel?: string; topPosition?: string;
maxLength?: number; layout?: "tv" | "mobile";
showPassword?: boolean; };
onShowPasswordChange?: (show: boolean) => void;
topPosition?: "3.5" | "4";
editable?: boolean;
autoComplete?:
| "password"
| "username"
| "name"
| "email"
| "tel"
| "url"
| "off";
autoCorrect?: boolean;
iconColor?: string;
}
export const PasswordInput: React.FC<PasswordInputProps> = ({ type PasswordVisibilityUncontrolled = {
value = "", value?: string;
onChangeText, onChangeText: (text: string) => void;
onSubmitEditing, placeholder: string;
placeholder, showPassword?: never;
className = "", onShowPasswordChange?: never;
testID, topPosition?: string;
accessibilityLabel, layout?: "tv" | "mobile";
maxLength = 500, };
showPassword: controlledShowPassword,
onShowPasswordChange,
topPosition = "3.5",
editable = true,
autoComplete,
autoCorrect,
iconColor = "white",
}) => {
const { t } = useTranslation();
const [internalShowPassword, setInternalShowPassword] = useState(false);
// Use controlled state if provided, otherwise use internal state type PasswordInputProps =
const showPassword = controlledShowPassword ?? internalShowPassword; | PasswordVisibilityControlled
const setShowPassword = onShowPasswordChange ?? setInternalShowPassword; | PasswordVisibilityUncontrolled;
// Construct Tailwind class from validated topPosition export const PasswordInput: React.FC<PasswordInputProps> = (props) => {
const topClass = `top-${topPosition}`; const {
value = "",
onChangeText,
placeholder,
topPosition = "3.5",
layout = "mobile",
} = props;
// Type guard to check if we're in controlled mode
const isControlled =
"showPassword" in props && "onShowPasswordChange" in props;
// For controlled mode, use the provided props
// For uncontrolled mode, use internal state (but we need to handle this differently)
const showPassword = isControlled
? (props as PasswordVisibilityControlled).showPassword
: false;
const handleTogglePassword = () => {
if (isControlled) {
(props as PasswordVisibilityControlled).onShowPasswordChange(
!showPassword,
);
}
// For uncontrolled mode, we could add internal state handling here if needed
};
// Generate top position with pixel precision
const getTopStyle = () => {
// Use pixel values directly
const positionInPx = parseFloat(topPosition);
return { top: positionInPx };
};
return ( return (
<View className={`relative ${className}`}> <>
<Input <Input
placeholder={placeholder} placeholder={placeholder}
onChangeText={onChangeText} onChangeText={onChangeText}
value={value} value={value}
secureTextEntry={!showPassword} secureTextEntry={!showPassword}
keyboardType='default' className='pr-4'
returnKeyType='done'
autoCapitalize='none'
textContentType='password'
clearButtonMode='while-editing'
maxLength={maxLength}
className='pr-12'
onSubmitEditing={onSubmitEditing}
testID={testID}
accessibilityLabel={accessibilityLabel}
editable={editable}
autoComplete={autoComplete}
autoCorrect={autoCorrect}
/> />
<TouchableOpacity <TouchableOpacity
onPress={() => setShowPassword(!showPassword)} onPress={handleTogglePassword}
className={`absolute right-3 ${topClass} p-1`} className={`absolute right-3 p-1 ${
accessible={true} layout === "tv" ? "h-10 justify-center" : ""
accessibilityRole='button' }`}
accessibilityLabel={ style={getTopStyle()}
showPassword
? t("accessibility.hide_password")
: t("accessibility.show_password")
}
accessibilityHint={t("accessibility.toggle_password_visibility")}
accessibilityState={{ selected: showPassword }}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
testID={testID ? `${testID}-toggle` : undefined}
> >
<Ionicons <Ionicons
name={showPassword ? "eye-off" : "eye"} name={showPassword ? "eye-off" : "eye"}
size={24} size={24}
color={iconColor} color='white'
/> />
</TouchableOpacity> </TouchableOpacity>
</View> </>
); );
}; };

View File

@@ -20,7 +20,7 @@ export function Input(props: InputProps) {
<TextInput <TextInput
ref={inputRef} ref={inputRef}
className={` className={`
w-full text-lg px-5 py-4 rounded-2xl w-full text-lg px-5 py-5 rounded-2xl
${isFocused ? "bg-neutral-700 border-2 border-white" : "bg-neutral-900 border-2 border-transparent"} ${isFocused ? "bg-neutral-700 border-2 border-white" : "bg-neutral-900 border-2 border-transparent"}
text-white ${extraClassName} text-white ${extraClassName}
`} `}
@@ -41,7 +41,7 @@ export function Input(props: InputProps) {
) : ( ) : (
<TextInput <TextInput
ref={inputRef} ref={inputRef}
className='p-4 rounded-xl bg-neutral-900' className='p-3 rounded-xl bg-neutral-900'
allowFontScaling={false} allowFontScaling={false}
style={[{ color: "white" }, style]} style={[{ color: "white" }, style]}
placeholderTextColor={"#9CA3AF"} placeholderTextColor={"#9CA3AF"}

View File

@@ -150,24 +150,20 @@ export const JellyseerrSettings = () => {
<Text className='font-bold mb-2'> <Text className='font-bold mb-2'>
{t("home.settings.plugins.jellyseerr.password")} {t("home.settings.plugins.jellyseerr.password")}
</Text> </Text>
<PasswordInput <View className='relative'>
value={jellyseerrPassword} <PasswordInput
onChangeText={setJellyseerrPassword} value={jellyseerrPassword}
placeholder={t( onChangeText={setJellyseerrPassword}
"home.settings.plugins.jellyseerr.password_placeholder", placeholder={t(
{ username: user?.Name }, "home.settings.plugins.jellyseerr.password_placeholder",
)} { username: user?.Name },
className='border border-neutral-800' )}
showPassword={showJellyseerrPassword} showPassword={showJellyseerrPassword}
onShowPasswordChange={setShowJellyseerrPassword} onShowPasswordChange={setShowJellyseerrPassword}
editable={!loginToJellyseerrMutation.isPending} layout='mobile'
autoComplete='password' topPosition='22'
autoCorrect={false} />
testID='jellyseerr-password-input' </View>
accessibilityLabel={t(
"home.settings.plugins.jellyseerr.password",
)}
/>
<Button <Button
loading={loginToJellyseerrMutation.isPending} loading={loginToJellyseerrMutation.isPending}
disabled={loginToJellyseerrMutation.isPending} disabled={loginToJellyseerrMutation.isPending}

View File

@@ -25,7 +25,7 @@
"@bottom-tabs/react-navigation": "^0.9.2", "@bottom-tabs/react-navigation": "^0.9.2",
"@expo/metro-runtime": "~5.0.4", "@expo/metro-runtime": "~5.0.4",
"@expo/react-native-action-sheet": "^4.1.1", "@expo/react-native-action-sheet": "^4.1.1",
"@expo/vector-icons": "15.0.2", "@expo/vector-icons": "^15.0.2",
"@gorhom/bottom-sheet": "^5.1.0", "@gorhom/bottom-sheet": "^5.1.0",
"@jellyfin/sdk": "^0.11.0", "@jellyfin/sdk": "^0.11.0",
"@kesha-antonov/react-native-background-downloader": "^3.2.6", "@kesha-antonov/react-native-background-downloader": "^3.2.6",