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 */}
<PasswordInput
value={credentials.password}
onChangeText={(text: string) =>
setCredentials({ ...credentials, password: text })
}
onSubmitEditing={handleLogin}
placeholder={t("login.password_placeholder")}
className='mb-4'
showPassword={showPassword}
onShowPasswordChange={setShowPassword}
topPosition='4'
testID='tv-password-input'
accessibilityLabel={t("login.password_placeholder")}
/>
<View className='relative'>
<PasswordInput
value={credentials.password}
onChangeText={(text: string) =>
setCredentials({ ...credentials, password: text })
}
placeholder={t("login.password_placeholder")}
showPassword={showPassword}
onShowPasswordChange={setShowPassword}
topPosition='4'
layout='tv'
/>
</View>
<View className='mt-4'>
<Button onPress={handleLogin}>{t("login.login_button")}</Button>
@@ -403,20 +402,22 @@ const Login: React.FC = () => {
textContentType='oneTimeCode'
clearButtonMode='while-editing'
maxLength={500}
extraClassName='mb-4'
/>
<PasswordInput
value={credentials.password}
onChangeText={(text) =>
setCredentials({ ...credentials, password: text })
}
placeholder={t("login.password_placeholder")}
showPassword={showPassword}
onShowPasswordChange={setShowPassword}
topPosition='3.5'
testID='mobile-password-input'
accessibilityLabel={t("login.password_placeholder")}
/>
<View className='relative mb-0.5'>
<PasswordInput
value={credentials.password}
onChangeText={(text) =>
setCredentials({ ...credentials, password: text })
}
placeholder={t("login.password_placeholder")}
showPassword={showPassword}
onShowPasswordChange={setShowPassword}
topPosition='10'
layout='mobile'
/>
</View>
<View className='flex flex-row items-center justify-between'>
<Button
onPress={handleLogin}
@@ -427,7 +428,7 @@ const Login: React.FC = () => {
</Button>
<TouchableOpacity
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
name='cellphone-lock'

View File

@@ -7,7 +7,7 @@
"@bottom-tabs/react-navigation": "^0.9.2",
"@expo/metro-runtime": "~5.0.4",
"@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",
"@jellyfin/sdk": "^0.11.0",
"@kesha-antonov/react-native-background-downloader": "^3.2.6",

View File

@@ -1,104 +1,90 @@
import { Ionicons } from "@expo/vector-icons";
import type React from "react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { TouchableOpacity, View } from "react-native";
import { TouchableOpacity } from "react-native";
import { Input } from "./common/Input";
interface PasswordInputProps {
// Discriminated union for password visibility control
type PasswordVisibilityControlled = {
value?: string;
onChangeText: (text: string) => void;
onSubmitEditing?: () => void;
placeholder: string;
className?: string;
testID?: string;
accessibilityLabel?: string;
maxLength?: number;
showPassword?: boolean;
onShowPasswordChange?: (show: boolean) => void;
topPosition?: "3.5" | "4";
editable?: boolean;
autoComplete?:
| "password"
| "username"
| "name"
| "email"
| "tel"
| "url"
| "off";
autoCorrect?: boolean;
iconColor?: string;
}
showPassword: boolean;
onShowPasswordChange: (show: boolean) => void;
topPosition?: string;
layout?: "tv" | "mobile";
};
export const PasswordInput: React.FC<PasswordInputProps> = ({
value = "",
onChangeText,
onSubmitEditing,
placeholder,
className = "",
testID,
accessibilityLabel,
maxLength = 500,
showPassword: controlledShowPassword,
onShowPasswordChange,
topPosition = "3.5",
editable = true,
autoComplete,
autoCorrect,
iconColor = "white",
}) => {
const { t } = useTranslation();
const [internalShowPassword, setInternalShowPassword] = useState(false);
type PasswordVisibilityUncontrolled = {
value?: string;
onChangeText: (text: string) => void;
placeholder: string;
showPassword?: never;
onShowPasswordChange?: never;
topPosition?: string;
layout?: "tv" | "mobile";
};
// Use controlled state if provided, otherwise use internal state
const showPassword = controlledShowPassword ?? internalShowPassword;
const setShowPassword = onShowPasswordChange ?? setInternalShowPassword;
type PasswordInputProps =
| PasswordVisibilityControlled
| PasswordVisibilityUncontrolled;
// Construct Tailwind class from validated topPosition
const topClass = `top-${topPosition}`;
export const PasswordInput: React.FC<PasswordInputProps> = (props) => {
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 (
<View className={`relative ${className}`}>
<>
<Input
placeholder={placeholder}
onChangeText={onChangeText}
value={value}
secureTextEntry={!showPassword}
keyboardType='default'
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}
className='pr-4'
/>
<TouchableOpacity
onPress={() => setShowPassword(!showPassword)}
className={`absolute right-3 ${topClass} p-1`}
accessible={true}
accessibilityRole='button'
accessibilityLabel={
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}
onPress={handleTogglePassword}
className={`absolute right-3 p-1 ${
layout === "tv" ? "h-10 justify-center" : ""
}`}
style={getTopStyle()}
>
<Ionicons
name={showPassword ? "eye-off" : "eye"}
size={24}
color={iconColor}
color='white'
/>
</TouchableOpacity>
</View>
</>
);
};

View File

@@ -20,7 +20,7 @@ export function Input(props: InputProps) {
<TextInput
ref={inputRef}
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"}
text-white ${extraClassName}
`}
@@ -41,7 +41,7 @@ export function Input(props: InputProps) {
) : (
<TextInput
ref={inputRef}
className='p-4 rounded-xl bg-neutral-900'
className='p-3 rounded-xl bg-neutral-900'
allowFontScaling={false}
style={[{ color: "white" }, style]}
placeholderTextColor={"#9CA3AF"}

View File

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

View File

@@ -25,7 +25,7 @@
"@bottom-tabs/react-navigation": "^0.9.2",
"@expo/metro-runtime": "~5.0.4",
"@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",
"@jellyfin/sdk": "^0.11.0",
"@kesha-antonov/react-native-background-downloader": "^3.2.6",