@idealyst/components 1.2.29 → 1.2.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/package.json +3 -3
- package/plugin/__tests__/web.test.ts +2 -2
- package/src/Accordion/Accordion.native.tsx +3 -2
- package/src/ActivityIndicator/ActivityIndicator.native.tsx +3 -2
- package/src/ActivityIndicator/ActivityIndicator.styles.tsx +25 -26
- package/src/ActivityIndicator/ActivityIndicator.web.tsx +2 -1
- package/src/Alert/Alert.native.tsx +20 -10
- package/src/Alert/Alert.styles.tsx +148 -86
- package/src/Alert/Alert.web.tsx +10 -5
- package/src/Alert/types.ts +53 -3
- package/src/Avatar/Avatar.native.tsx +3 -2
- package/src/Avatar/Avatar.web.tsx +2 -1
- package/src/Avatar/types.ts +1 -1
- package/src/Badge/Badge.native.tsx +18 -6
- package/src/Badge/Badge.styles.tsx +22 -5
- package/src/Badge/Badge.web.tsx +12 -4
- package/src/Badge/types.ts +14 -2
- package/src/Breadcrumb/Breadcrumb.native.tsx +3 -2
- package/src/Button/Button.native.tsx +16 -6
- package/src/Button/Button.styles.tsx +2 -2
- package/src/Button/Button.web.tsx +19 -15
- package/src/Button/types.ts +6 -10
- package/src/Card/Card.native.tsx +27 -3
- package/src/Card/Card.web.tsx +30 -4
- package/src/Card/types.ts +15 -0
- package/src/Checkbox/Checkbox.native.tsx +5 -4
- package/src/Checkbox/Checkbox.styles.tsx +62 -52
- package/src/Checkbox/Checkbox.web.tsx +4 -3
- package/src/Checkbox/types.ts +1 -1
- package/src/Chip/Chip.native.tsx +30 -7
- package/src/Chip/Chip.styles.tsx +142 -124
- package/src/Chip/Chip.web.tsx +28 -5
- package/src/Chip/types.ts +15 -0
- package/src/Dialog/Dialog.native.tsx +6 -6
- package/src/Dialog/Dialog.web.tsx +5 -5
- package/src/Dialog/types.ts +2 -2
- package/src/Divider/Divider.native.tsx +20 -17
- package/src/Divider/Divider.styles.tsx +51 -29
- package/src/Divider/Divider.web.tsx +5 -4
- package/src/Divider/types.ts +3 -3
- package/src/Icon/Icon.native.tsx +3 -2
- package/src/Icon/Icon.web.tsx +2 -1
- package/src/Icon/IconSvg/IconSvg.native.tsx +3 -2
- package/src/Image/Image.native.tsx +3 -2
- package/src/Input/Input.native.tsx +42 -290
- package/src/Input/Input.styles.tsx +1 -1
- package/src/Input/Input.web.tsx +37 -288
- package/src/Input/index.native.ts +9 -2
- package/src/Input/index.ts +8 -1
- package/src/Input/index.web.ts +8 -1
- package/src/Input/types.ts +1 -1
- package/src/List/List.native.tsx +3 -2
- package/src/List/ListItem.native.tsx +3 -2
- package/src/List/ListSection.native.tsx +3 -2
- package/src/Menu/Menu.native.tsx +2 -1
- package/src/Menu/Menu.styles.tsx +79 -29
- package/src/Menu/Menu.web.tsx +2 -1
- package/src/Menu/MenuItem.native.tsx +4 -3
- package/src/Menu/MenuItem.styles.tsx +81 -32
- package/src/Menu/MenuItem.web.tsx +2 -1
- package/src/Menu/docs.ts +1 -1
- package/src/Popover/Popover.native.tsx +2 -1
- package/src/Popover/Popover.web.tsx +2 -1
- package/src/Popover/types.ts +15 -4
- package/src/Pressable/Pressable.native.tsx +3 -2
- package/src/Pressable/Pressable.web.tsx +3 -5
- package/src/Progress/Progress.native.tsx +5 -4
- package/src/Progress/Progress.web.tsx +3 -3
- package/src/Progress/types.ts +3 -3
- package/src/RadioButton/RadioButton.native.tsx +4 -3
- package/src/RadioButton/RadioButton.styles.tsx +53 -33
- package/src/RadioButton/RadioGroup.native.tsx +3 -2
- package/src/SVGImage/SVGImage.native.tsx +5 -4
- package/src/SVGImage/SVGImage.styles.tsx +44 -10
- package/src/SVGImage/SVGImage.web.tsx +2 -1
- package/src/Screen/Screen.native.tsx +2 -1
- package/src/Screen/Screen.web.tsx +2 -1
- package/src/Select/Select.native.tsx +6 -5
- package/src/Select/Select.styles.tsx +1 -1
- package/src/Select/Select.web.tsx +4 -3
- package/src/Select/types.ts +1 -1
- package/src/Skeleton/Skeleton.native.tsx +2 -1
- package/src/Slider/Slider.native.tsx +9 -8
- package/src/Slider/Slider.web.tsx +10 -9
- package/src/Slider/types.ts +9 -2
- package/src/Switch/Switch.native.tsx +7 -6
- package/src/Switch/Switch.styles.tsx +35 -17
- package/src/Switch/Switch.web.tsx +8 -7
- package/src/Switch/types.ts +44 -4
- package/src/TabBar/TabBar.native.tsx +3 -2
- package/src/Text/Text.native.tsx +3 -2
- package/src/Text/Text.web.tsx +2 -1
- package/src/TextArea/TextArea.native.tsx +3 -2
- package/src/TextArea/TextArea.styles.tsx +2 -2
- package/src/TextArea/TextArea.web.tsx +2 -1
- package/src/TextInput/TextInput.native.tsx +300 -0
- package/src/TextInput/TextInput.styles.tsx +207 -0
- package/src/TextInput/TextInput.web.tsx +301 -0
- package/src/TextInput/index.native.ts +3 -0
- package/src/TextInput/index.ts +5 -0
- package/src/TextInput/index.web.ts +5 -0
- package/src/TextInput/types.ts +163 -0
- package/src/Tooltip/Tooltip.native.tsx +3 -2
- package/src/Video/Video.native.tsx +4 -3
- package/src/View/View.native.tsx +2 -1
- package/src/View/View.web.tsx +2 -1
- package/src/examples/AlertExamples.tsx +5 -5
- package/src/examples/ButtonExamples.tsx +12 -12
- package/src/examples/CardExamples.tsx +1 -1
- package/src/examples/CheckboxExamples.tsx +2 -2
- package/src/examples/ChipExamples.tsx +6 -6
- package/src/examples/DialogExamples.tsx +1 -1
- package/src/examples/DividerExamples.tsx +1 -1
- package/src/examples/InputExamples.tsx +1 -1
- package/src/examples/LinkExamples.tsx +1 -1
- package/src/examples/ListExamples.tsx +1 -1
- package/src/examples/MenuExamples.tsx +2 -2
- package/src/examples/ProgressExamples.tsx +1 -1
- package/src/examples/RadioButtonExamples.tsx +5 -5
- package/src/examples/SVGImageExamples.tsx +1 -1
- package/src/examples/SelectExamples.tsx +1 -1
- package/src/examples/SliderExamples.tsx +5 -5
- package/src/examples/SwitchExamples.tsx +2 -2
- package/src/examples/TableExamples.tsx +1 -1
- package/src/examples/TooltipExamples.tsx +2 -2
- package/src/extensions/index.ts +1 -0
- package/src/extensions/types.ts +10 -3
- package/src/index.ts +23 -2
- package/src/utils/index.ts +12 -0
- package/src/utils/refTypes.ts +50 -0
|
@@ -6,21 +6,22 @@ import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
|
|
|
6
6
|
import { isIconName } from '../Icon/icon-resolver';
|
|
7
7
|
import useMergeRefs from '../hooks/useMergeRefs';
|
|
8
8
|
import { getWebSelectionAriaProps, generateAccessibilityId } from '../utils/accessibility';
|
|
9
|
+
import type { IdealystElement } from '../utils/refTypes';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Toggle switch for binary on/off states with optional label and icons.
|
|
12
13
|
* Supports custom enabled/disabled icons and multiple sizes.
|
|
13
14
|
*/
|
|
14
|
-
const Switch = forwardRef<
|
|
15
|
+
const Switch = forwardRef<IdealystElement, SwitchProps>(({
|
|
15
16
|
checked = false,
|
|
16
|
-
|
|
17
|
+
onChange,
|
|
17
18
|
disabled = false,
|
|
18
19
|
label,
|
|
19
20
|
labelPosition = 'right',
|
|
20
21
|
intent = 'primary',
|
|
21
22
|
size = 'md',
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
onIcon,
|
|
24
|
+
offIcon,
|
|
24
25
|
// Spacing variants from FormInputStyleProps
|
|
25
26
|
margin,
|
|
26
27
|
marginVertical,
|
|
@@ -39,8 +40,8 @@ const Switch = forwardRef<HTMLDivElement | HTMLButtonElement, SwitchProps>(({
|
|
|
39
40
|
accessibilityChecked,
|
|
40
41
|
}, ref) => {
|
|
41
42
|
const handleClick = () => {
|
|
42
|
-
if (!disabled &&
|
|
43
|
-
|
|
43
|
+
if (!disabled && onChange) {
|
|
44
|
+
onChange(!checked);
|
|
44
45
|
}
|
|
45
46
|
};
|
|
46
47
|
|
|
@@ -93,7 +94,7 @@ const Switch = forwardRef<HTMLDivElement | HTMLButtonElement, SwitchProps>(({
|
|
|
93
94
|
|
|
94
95
|
// Helper to render icon
|
|
95
96
|
const renderIcon = () => {
|
|
96
|
-
const iconToRender = checked ?
|
|
97
|
+
const iconToRender = checked ? onIcon : offIcon;
|
|
97
98
|
if (!iconToRender) return null;
|
|
98
99
|
|
|
99
100
|
if (isIconName(iconToRender)) {
|
package/src/Switch/types.ts
CHANGED
|
@@ -10,21 +10,61 @@ export type SwitchSizeVariant = Size;
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Toggle switch component for binary on/off states.
|
|
13
|
-
* Supports custom icons for
|
|
13
|
+
* Supports custom icons for on/off states and optional label positioning.
|
|
14
14
|
*/
|
|
15
15
|
export interface SwitchProps extends FormInputStyleProps, SelectionAccessibilityProps {
|
|
16
16
|
/**
|
|
17
17
|
* Whether the switch is on
|
|
18
18
|
*/
|
|
19
19
|
checked?: boolean;
|
|
20
|
-
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Called when the switch state changes
|
|
23
|
+
*/
|
|
24
|
+
onChange?: (checked: boolean) => void;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Whether the switch is disabled
|
|
28
|
+
*/
|
|
21
29
|
disabled?: boolean;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Label text to display next to the switch
|
|
33
|
+
*/
|
|
22
34
|
label?: string;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Position of the label relative to the switch
|
|
38
|
+
*/
|
|
23
39
|
labelPosition?: 'left' | 'right';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The intent/color scheme of the switch
|
|
43
|
+
*/
|
|
24
44
|
intent?: SwitchIntentVariant;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Size of the switch
|
|
48
|
+
*/
|
|
25
49
|
size?: SwitchSizeVariant;
|
|
26
|
-
|
|
27
|
-
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Icon to display in the thumb when the switch is ON
|
|
53
|
+
*/
|
|
54
|
+
onIcon?: IconName | React.ReactNode;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Icon to display in the thumb when the switch is OFF
|
|
58
|
+
*/
|
|
59
|
+
offIcon?: IconName | React.ReactNode;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Additional styles (platform-specific)
|
|
63
|
+
*/
|
|
28
64
|
style?: StyleProp<ViewStyle>;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Test ID for testing
|
|
68
|
+
*/
|
|
29
69
|
testID?: string;
|
|
30
70
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from './TabBar.styles';
|
|
11
11
|
import type { TabBarProps, TabBarItem } from './types';
|
|
12
12
|
import { getNativeAccessibilityProps } from '../utils/accessibility';
|
|
13
|
+
import type { IdealystElement } from '../utils/refTypes';
|
|
13
14
|
|
|
14
15
|
// Icon size mapping based on size variant
|
|
15
16
|
const ICON_SIZES: Record<string, number> = {
|
|
@@ -33,7 +34,7 @@ function renderIcon(
|
|
|
33
34
|
return icon;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
const TabBar = forwardRef<
|
|
37
|
+
const TabBar = forwardRef<IdealystElement, TabBarProps>(({
|
|
37
38
|
items,
|
|
38
39
|
value: controlledValue,
|
|
39
40
|
defaultValue,
|
|
@@ -154,7 +155,7 @@ const TabBar = forwardRef<View, TabBarProps>(({
|
|
|
154
155
|
}}
|
|
155
156
|
style={{ width: '100%' }}
|
|
156
157
|
>
|
|
157
|
-
<View ref={ref} nativeID={id} style={[containerStyle, style]} testID={testID} {...nativeA11yProps}>
|
|
158
|
+
<View ref={ref as any} nativeID={id} style={[containerStyle, style]} testID={testID} {...nativeA11yProps}>
|
|
158
159
|
{/* Animated indicator - render first so it's behind */}
|
|
159
160
|
<Animated.View
|
|
160
161
|
style={[
|
package/src/Text/Text.native.tsx
CHANGED
|
@@ -2,8 +2,9 @@ import { forwardRef } from 'react';
|
|
|
2
2
|
import { Text as RNText } from 'react-native';
|
|
3
3
|
import { TextProps } from './types';
|
|
4
4
|
import { textStyles } from './Text.styles';
|
|
5
|
+
import type { IdealystElement } from '../utils/refTypes';
|
|
5
6
|
|
|
6
|
-
const Text = forwardRef<
|
|
7
|
+
const Text = forwardRef<IdealystElement, TextProps>(({
|
|
7
8
|
children,
|
|
8
9
|
typography = 'body1',
|
|
9
10
|
weight,
|
|
@@ -30,7 +31,7 @@ const Text = forwardRef<RNText, TextProps>(({
|
|
|
30
31
|
|
|
31
32
|
return (
|
|
32
33
|
<RNText
|
|
33
|
-
ref={ref}
|
|
34
|
+
ref={ref as any}
|
|
34
35
|
nativeID={id}
|
|
35
36
|
style={[textStyles.text({ color, typography, weight, align }), style]}
|
|
36
37
|
testID={testID}
|
package/src/Text/Text.web.tsx
CHANGED
|
@@ -3,12 +3,13 @@ import { getWebProps } from 'react-native-unistyles/web';
|
|
|
3
3
|
import { TextProps } from './types';
|
|
4
4
|
import { textStyles } from './Text.styles';
|
|
5
5
|
import useMergeRefs from '../hooks/useMergeRefs';
|
|
6
|
+
import type { IdealystElement } from '../utils/refTypes';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Typography component for displaying text with predefined styles and semantic variants.
|
|
9
10
|
* Supports multiple typography scales, colors, weights, and alignments.
|
|
10
11
|
*/
|
|
11
|
-
const Text = forwardRef<
|
|
12
|
+
const Text = forwardRef<IdealystElement, TextProps>(({
|
|
12
13
|
children,
|
|
13
14
|
typography,
|
|
14
15
|
weight,
|
|
@@ -4,8 +4,9 @@ import { textAreaStyles } from './TextArea.styles';
|
|
|
4
4
|
import Text from '../Text';
|
|
5
5
|
import type { TextAreaProps } from './types';
|
|
6
6
|
import { getNativeFormAccessibilityProps } from '../utils/accessibility';
|
|
7
|
+
import type { IdealystElement } from '../utils/refTypes';
|
|
7
8
|
|
|
8
|
-
const TextArea = forwardRef<
|
|
9
|
+
const TextArea = forwardRef<IdealystElement, TextAreaProps>(({
|
|
9
10
|
value: controlledValue,
|
|
10
11
|
defaultValue = '',
|
|
11
12
|
onChange,
|
|
@@ -149,7 +150,7 @@ const TextArea = forwardRef<TextInput, TextAreaProps>(({
|
|
|
149
150
|
|
|
150
151
|
<View style={textareaContainerStyleComputed}>
|
|
151
152
|
<TextInput
|
|
152
|
-
ref={ref}
|
|
153
|
+
ref={ref as any}
|
|
153
154
|
{...nativeA11yProps}
|
|
154
155
|
style={[
|
|
155
156
|
textareaStyleComputed,
|
|
@@ -117,7 +117,7 @@ export const textAreaStyles = defineStyle('TextArea', (theme: Theme) => ({
|
|
|
117
117
|
fontSize: 12,
|
|
118
118
|
variants: {
|
|
119
119
|
hasError: {
|
|
120
|
-
true: { color: theme.intents.
|
|
120
|
+
true: { color: theme.intents.danger.primary },
|
|
121
121
|
false: { color: theme.colors.text.secondary },
|
|
122
122
|
},
|
|
123
123
|
},
|
|
@@ -136,7 +136,7 @@ export const textAreaStyles = defineStyle('TextArea', (theme: Theme) => ({
|
|
|
136
136
|
color: theme.colors.text.secondary,
|
|
137
137
|
variants: {
|
|
138
138
|
isAtLimit: {
|
|
139
|
-
true: { color: theme.intents.
|
|
139
|
+
true: { color: theme.intents.danger.primary },
|
|
140
140
|
false: {},
|
|
141
141
|
},
|
|
142
142
|
isNearLimit: {
|
|
@@ -4,12 +4,13 @@ import { textAreaStyles } from './TextArea.styles';
|
|
|
4
4
|
import type { TextAreaProps } from './types';
|
|
5
5
|
import useMergeRefs from '../hooks/useMergeRefs';
|
|
6
6
|
import { getWebFormAriaProps, combineIds, generateAccessibilityId } from '../utils/accessibility';
|
|
7
|
+
import type { IdealystElement } from '../utils/refTypes';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Multi-line text input with auto-grow, character counting, and validation support.
|
|
10
11
|
* Includes label, helper text, and error message display.
|
|
11
12
|
*/
|
|
12
|
-
const TextArea = forwardRef<
|
|
13
|
+
const TextArea = forwardRef<IdealystElement, TextAreaProps>(({
|
|
13
14
|
value: controlledValue,
|
|
14
15
|
defaultValue = '',
|
|
15
16
|
onChange,
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import React, { useState, isValidElement, useMemo, useEffect, useRef, useCallback } from 'react';
|
|
2
|
+
import { View, TextInput as RNTextInput, TouchableOpacity, Platform, TextInputProps as RNTextInputProps } from 'react-native';
|
|
3
|
+
import MaterialDesignIcons from '@react-native-vector-icons/material-design-icons';
|
|
4
|
+
import { useUnistyles } from 'react-native-unistyles';
|
|
5
|
+
import { TextInputProps } from './types';
|
|
6
|
+
import { textInputStyles } from './TextInput.styles';
|
|
7
|
+
import { getNativeFormAccessibilityProps } from '../utils/accessibility';
|
|
8
|
+
import type { IdealystElement } from '../utils/refTypes';
|
|
9
|
+
|
|
10
|
+
// Inner TextInput component that can be memoized to prevent re-renders
|
|
11
|
+
// for Android secure text entry
|
|
12
|
+
type InnerTextInputProps = {
|
|
13
|
+
inputRef: React.ForwardedRef<RNTextInput>;
|
|
14
|
+
value: string | undefined;
|
|
15
|
+
onChangeText: ((text: string) => void) | undefined;
|
|
16
|
+
isAndroidSecure: boolean;
|
|
17
|
+
textInputProps: Omit<RNTextInputProps, 'value' | 'defaultValue' | 'onChangeText'>;
|
|
18
|
+
inputStyle: any;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const InnerRNTextInput = React.memo<InnerTextInputProps>(
|
|
22
|
+
({ inputRef, value, onChangeText, isAndroidSecure, textInputProps, inputStyle }) => {
|
|
23
|
+
return (
|
|
24
|
+
<RNTextInput
|
|
25
|
+
ref={inputRef as any}
|
|
26
|
+
// For Android secure text entry, don't pass value prop at all
|
|
27
|
+
// Let TextInput manage its own state to preserve character reveal animation
|
|
28
|
+
{...(isAndroidSecure ? {} : { value })}
|
|
29
|
+
onChangeText={onChangeText}
|
|
30
|
+
style={inputStyle}
|
|
31
|
+
{...textInputProps}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
},
|
|
35
|
+
(prevProps, nextProps) => {
|
|
36
|
+
// For Android secure text entry, skip re-renders when only value changes
|
|
37
|
+
if (nextProps.isAndroidSecure) {
|
|
38
|
+
// Only re-render if non-value props change
|
|
39
|
+
const valueChanged = prevProps.value !== nextProps.value;
|
|
40
|
+
const otherPropsChanged =
|
|
41
|
+
prevProps.onChangeText !== nextProps.onChangeText ||
|
|
42
|
+
prevProps.isAndroidSecure !== nextProps.isAndroidSecure ||
|
|
43
|
+
prevProps.textInputProps !== nextProps.textInputProps ||
|
|
44
|
+
prevProps.inputStyle !== nextProps.inputStyle;
|
|
45
|
+
|
|
46
|
+
if (valueChanged && !otherPropsChanged) {
|
|
47
|
+
return true; // Skip re-render
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return false; // Allow re-render
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const TextInput = React.forwardRef<IdealystElement, TextInputProps>(({
|
|
55
|
+
value,
|
|
56
|
+
onChangeText,
|
|
57
|
+
onFocus,
|
|
58
|
+
onBlur,
|
|
59
|
+
onPress,
|
|
60
|
+
placeholder,
|
|
61
|
+
disabled = false,
|
|
62
|
+
inputMode = 'text',
|
|
63
|
+
secureTextEntry = false,
|
|
64
|
+
leftIcon,
|
|
65
|
+
rightIcon,
|
|
66
|
+
showPasswordToggle,
|
|
67
|
+
autoCapitalize = 'sentences',
|
|
68
|
+
size = 'md',
|
|
69
|
+
type = 'outlined',
|
|
70
|
+
hasError = false,
|
|
71
|
+
// Spacing variants from FormInputStyleProps
|
|
72
|
+
margin,
|
|
73
|
+
marginVertical,
|
|
74
|
+
marginHorizontal,
|
|
75
|
+
style,
|
|
76
|
+
testID,
|
|
77
|
+
id,
|
|
78
|
+
// Accessibility props
|
|
79
|
+
accessibilityLabel,
|
|
80
|
+
accessibilityHint,
|
|
81
|
+
accessibilityDisabled,
|
|
82
|
+
accessibilityHidden,
|
|
83
|
+
accessibilityRole,
|
|
84
|
+
accessibilityRequired,
|
|
85
|
+
accessibilityInvalid,
|
|
86
|
+
}, ref) => {
|
|
87
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
88
|
+
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
|
89
|
+
|
|
90
|
+
// Track if this is a secure field that needs Android workaround
|
|
91
|
+
const isSecureField = inputMode === 'password' || secureTextEntry;
|
|
92
|
+
const needsAndroidSecureWorkaround = Platform.OS === 'android' && isSecureField && !isPasswordVisible;
|
|
93
|
+
|
|
94
|
+
// For Android secure text entry, we use an internal ref to track value
|
|
95
|
+
const internalValueRef = useRef(value ?? '');
|
|
96
|
+
|
|
97
|
+
// Sync external value changes to internal ref (for programmatic updates)
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (value !== undefined) {
|
|
100
|
+
internalValueRef.current = value;
|
|
101
|
+
}
|
|
102
|
+
}, [value]);
|
|
103
|
+
|
|
104
|
+
// Get theme for icon sizes and colors
|
|
105
|
+
const { theme } = useUnistyles();
|
|
106
|
+
const iconSize = theme.sizes.input[size].iconSize;
|
|
107
|
+
const iconColor = theme.colors.text.secondary;
|
|
108
|
+
|
|
109
|
+
// Determine if we should show password toggle
|
|
110
|
+
const isPasswordField = inputMode === 'password' || secureTextEntry;
|
|
111
|
+
const shouldShowPasswordToggle = isPasswordField && (showPasswordToggle !== false);
|
|
112
|
+
|
|
113
|
+
const getKeyboardType = useCallback((): 'default' | 'email-address' | 'numeric' => {
|
|
114
|
+
switch (inputMode) {
|
|
115
|
+
case 'email':
|
|
116
|
+
return 'email-address';
|
|
117
|
+
case 'number':
|
|
118
|
+
return 'numeric';
|
|
119
|
+
case 'password':
|
|
120
|
+
case 'text':
|
|
121
|
+
default:
|
|
122
|
+
return 'default';
|
|
123
|
+
}
|
|
124
|
+
}, [inputMode]);
|
|
125
|
+
|
|
126
|
+
const handleFocus = useCallback(() => {
|
|
127
|
+
setIsFocused(true);
|
|
128
|
+
onFocus?.();
|
|
129
|
+
}, [onFocus]);
|
|
130
|
+
|
|
131
|
+
const handlePress = useCallback(() => {
|
|
132
|
+
onPress?.();
|
|
133
|
+
}, [onPress]);
|
|
134
|
+
|
|
135
|
+
const handleBlur = useCallback(() => {
|
|
136
|
+
setIsFocused(false);
|
|
137
|
+
onBlur?.();
|
|
138
|
+
}, [onBlur]);
|
|
139
|
+
|
|
140
|
+
const togglePasswordVisibility = () => {
|
|
141
|
+
setIsPasswordVisible(!isPasswordVisible);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Memoized change handler for InnerTextInput
|
|
145
|
+
const handleChangeText = useCallback((text: string) => {
|
|
146
|
+
internalValueRef.current = text;
|
|
147
|
+
onChangeText?.(text);
|
|
148
|
+
}, [onChangeText]);
|
|
149
|
+
|
|
150
|
+
// Memoized input style
|
|
151
|
+
const inputStyle = useMemo(() => (textInputStyles.input as any)({}), []);
|
|
152
|
+
|
|
153
|
+
// Generate native accessibility props
|
|
154
|
+
const nativeA11yProps = useMemo(() => {
|
|
155
|
+
// Derive invalid state from hasError or explicit accessibilityInvalid
|
|
156
|
+
const isInvalid = accessibilityInvalid ?? hasError;
|
|
157
|
+
|
|
158
|
+
return getNativeFormAccessibilityProps({
|
|
159
|
+
accessibilityLabel,
|
|
160
|
+
accessibilityHint,
|
|
161
|
+
accessibilityDisabled: accessibilityDisabled ?? disabled,
|
|
162
|
+
accessibilityHidden,
|
|
163
|
+
accessibilityRole: accessibilityRole ?? 'textbox',
|
|
164
|
+
accessibilityRequired,
|
|
165
|
+
accessibilityInvalid: isInvalid,
|
|
166
|
+
});
|
|
167
|
+
}, [
|
|
168
|
+
accessibilityLabel,
|
|
169
|
+
accessibilityHint,
|
|
170
|
+
accessibilityDisabled,
|
|
171
|
+
disabled,
|
|
172
|
+
accessibilityHidden,
|
|
173
|
+
accessibilityRole,
|
|
174
|
+
accessibilityRequired,
|
|
175
|
+
accessibilityInvalid,
|
|
176
|
+
hasError,
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
// Memoized TextInput props (everything except value/onChangeText)
|
|
180
|
+
const textInputProps = useMemo(() => ({
|
|
181
|
+
onPress: handlePress,
|
|
182
|
+
placeholder,
|
|
183
|
+
editable: !disabled,
|
|
184
|
+
keyboardType: getKeyboardType(),
|
|
185
|
+
secureTextEntry: isSecureField && !isPasswordVisible,
|
|
186
|
+
autoCapitalize,
|
|
187
|
+
onFocus: handleFocus,
|
|
188
|
+
onBlur: handleBlur,
|
|
189
|
+
placeholderTextColor: '#999999',
|
|
190
|
+
...nativeA11yProps,
|
|
191
|
+
}), [
|
|
192
|
+
handlePress,
|
|
193
|
+
placeholder,
|
|
194
|
+
disabled,
|
|
195
|
+
getKeyboardType,
|
|
196
|
+
isSecureField,
|
|
197
|
+
isPasswordVisible,
|
|
198
|
+
autoCapitalize,
|
|
199
|
+
handleFocus,
|
|
200
|
+
handleBlur,
|
|
201
|
+
nativeA11yProps,
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
// Apply variants to the stylesheet (for size and spacing)
|
|
205
|
+
textInputStyles.useVariants({
|
|
206
|
+
size,
|
|
207
|
+
margin,
|
|
208
|
+
marginVertical,
|
|
209
|
+
marginHorizontal,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Compute dynamic styles - call as functions for theme reactivity
|
|
213
|
+
const containerStyle = (textInputStyles.container as any)({ type, focused: isFocused, hasError, disabled });
|
|
214
|
+
const leftIconContainerStyle = (textInputStyles.leftIconContainer as any)({});
|
|
215
|
+
const rightIconContainerStyle = (textInputStyles.rightIconContainer as any)({});
|
|
216
|
+
const passwordToggleStyle = (textInputStyles.passwordToggle as any)({});
|
|
217
|
+
|
|
218
|
+
// Helper to render left icon
|
|
219
|
+
const renderLeftIcon = () => {
|
|
220
|
+
if (!leftIcon) return null;
|
|
221
|
+
|
|
222
|
+
if (typeof leftIcon === 'string') {
|
|
223
|
+
return (
|
|
224
|
+
<MaterialDesignIcons
|
|
225
|
+
name={leftIcon}
|
|
226
|
+
size={iconSize}
|
|
227
|
+
color={iconColor}
|
|
228
|
+
/>
|
|
229
|
+
);
|
|
230
|
+
} else if (isValidElement(leftIcon)) {
|
|
231
|
+
return leftIcon;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return null;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// Helper to render right icon (not password toggle)
|
|
238
|
+
const renderRightIcon = () => {
|
|
239
|
+
if (!rightIcon) return null;
|
|
240
|
+
|
|
241
|
+
if (typeof rightIcon === 'string') {
|
|
242
|
+
return (
|
|
243
|
+
<MaterialDesignIcons
|
|
244
|
+
name={rightIcon}
|
|
245
|
+
size={iconSize}
|
|
246
|
+
color={iconColor}
|
|
247
|
+
/>
|
|
248
|
+
);
|
|
249
|
+
} else if (isValidElement(rightIcon)) {
|
|
250
|
+
return rightIcon;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return null;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<View style={[containerStyle, style]} testID={testID} nativeID={id}>
|
|
258
|
+
{/* Left Icon */}
|
|
259
|
+
{leftIcon && (
|
|
260
|
+
<View style={leftIconContainerStyle}>
|
|
261
|
+
{renderLeftIcon()}
|
|
262
|
+
</View>
|
|
263
|
+
)}
|
|
264
|
+
|
|
265
|
+
{/* Input */}
|
|
266
|
+
<InnerRNTextInput
|
|
267
|
+
inputRef={ref}
|
|
268
|
+
value={value}
|
|
269
|
+
onChangeText={handleChangeText}
|
|
270
|
+
isAndroidSecure={needsAndroidSecureWorkaround}
|
|
271
|
+
inputStyle={inputStyle}
|
|
272
|
+
textInputProps={textInputProps}
|
|
273
|
+
/>
|
|
274
|
+
|
|
275
|
+
{/* Right Icon or Password Toggle */}
|
|
276
|
+
{shouldShowPasswordToggle ? (
|
|
277
|
+
<TouchableOpacity
|
|
278
|
+
style={passwordToggleStyle}
|
|
279
|
+
onPress={togglePasswordVisibility}
|
|
280
|
+
disabled={disabled}
|
|
281
|
+
accessibilityLabel={isPasswordVisible ? 'Hide password' : 'Show password'}
|
|
282
|
+
>
|
|
283
|
+
<MaterialDesignIcons
|
|
284
|
+
name={isPasswordVisible ? 'eye-off' : 'eye'}
|
|
285
|
+
size={iconSize}
|
|
286
|
+
color={iconColor}
|
|
287
|
+
/>
|
|
288
|
+
</TouchableOpacity>
|
|
289
|
+
) : rightIcon ? (
|
|
290
|
+
<View style={rightIconContainerStyle}>
|
|
291
|
+
{renderRightIcon()}
|
|
292
|
+
</View>
|
|
293
|
+
) : null}
|
|
294
|
+
</View>
|
|
295
|
+
);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
TextInput.displayName = 'TextInput';
|
|
299
|
+
|
|
300
|
+
export default TextInput;
|