@idealyst/components 1.1.7 → 1.1.8
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/package.json +3 -3
- package/src/Accordion/Accordion.native.tsx +8 -6
- package/src/Accordion/Accordion.styles.old.tsx +298 -0
- package/src/Accordion/Accordion.styles.tsx +102 -236
- package/src/ActivityIndicator/ActivityIndicator.styles.old.tsx +94 -0
- package/src/ActivityIndicator/ActivityIndicator.styles.tsx +44 -74
- package/src/Alert/Alert.native.tsx +16 -6
- package/src/Alert/Alert.styles.old.tsx +209 -0
- package/src/Alert/Alert.styles.tsx +67 -149
- package/src/Avatar/Avatar.styles.old.tsx +99 -0
- package/src/Avatar/Avatar.styles.tsx +35 -80
- package/src/Badge/Badge.styles.old.tsx +157 -0
- package/src/Badge/Badge.styles.tsx +61 -121
- package/src/Breadcrumb/Breadcrumb.styles.old.tsx +231 -0
- package/src/Breadcrumb/Breadcrumb.styles.tsx +83 -200
- package/src/Breadcrumb/Breadcrumb.web.tsx +28 -23
- package/src/Button/Button.styles.tsx +89 -141
- package/src/Card/Card.native.tsx +7 -11
- package/src/Card/Card.styles.old.tsx +160 -0
- package/src/Card/Card.styles.tsx +105 -142
- package/src/Card/Card.web.tsx +5 -4
- package/src/Checkbox/Checkbox.native.tsx +9 -5
- package/src/Checkbox/Checkbox.styles.old.tsx +271 -0
- package/src/Checkbox/Checkbox.styles.tsx +104 -216
- package/src/Checkbox/Checkbox.web.tsx +6 -6
- package/src/Chip/Chip.styles.old.tsx +184 -0
- package/src/Chip/Chip.styles.tsx +34 -72
- package/src/Dialog/Dialog.native.tsx +7 -4
- package/src/Dialog/Dialog.styles.old.tsx +202 -0
- package/src/Dialog/Dialog.styles.tsx +69 -133
- package/src/Divider/Divider.styles.old.tsx +172 -0
- package/src/Divider/Divider.styles.tsx +62 -84
- package/src/Icon/Icon.native.tsx +8 -8
- package/src/Icon/Icon.styles.old.tsx +81 -0
- package/src/Icon/Icon.styles.tsx +52 -66
- package/src/Icon/Icon.web.tsx +43 -7
- package/src/Icon/IconSvg/IconSvg.web.tsx +2 -0
- package/src/Image/Image.styles.old.tsx +69 -0
- package/src/Image/Image.styles.tsx +46 -60
- package/src/Input/Input.native.tsx +138 -53
- package/src/Input/Input.styles.old.tsx +289 -0
- package/src/Input/Input.styles.tsx +127 -198
- package/src/List/List.native.tsx +5 -2
- package/src/List/List.styles.old.tsx +242 -0
- package/src/List/List.styles.tsx +179 -215
- package/src/List/ListItem.native.tsx +12 -6
- package/src/List/ListItem.web.tsx +23 -13
- package/src/Menu/Menu.styles.old.tsx +197 -0
- package/src/Menu/Menu.styles.tsx +68 -150
- package/src/Menu/MenuItem.native.tsx +5 -3
- package/src/Menu/MenuItem.styles.old.tsx +114 -0
- package/src/Menu/MenuItem.styles.tsx +57 -89
- package/src/Menu/MenuItem.web.tsx +8 -3
- package/src/Popover/Popover.native.tsx +10 -4
- package/src/Popover/Popover.styles.old.tsx +135 -0
- package/src/Popover/Popover.styles.tsx +51 -112
- package/src/Pressable/Pressable.styles.old.tsx +27 -0
- package/src/Pressable/Pressable.styles.tsx +35 -27
- package/src/Progress/Progress.styles.old.tsx +200 -0
- package/src/Progress/Progress.styles.tsx +75 -164
- package/src/RadioButton/RadioButton.native.tsx +4 -3
- package/src/RadioButton/RadioButton.styles.old.tsx +175 -0
- package/src/RadioButton/RadioButton.styles.tsx +83 -154
- package/src/RadioButton/RadioButton.web.tsx +2 -2
- package/src/SVGImage/SVGImage.styles.old.tsx +86 -0
- package/src/SVGImage/SVGImage.styles.tsx +35 -78
- package/src/Screen/Screen.native.tsx +18 -25
- package/src/Screen/Screen.styles.old.tsx +87 -0
- package/src/Screen/Screen.styles.tsx +105 -67
- package/src/Screen/Screen.web.tsx +1 -1
- package/src/Select/Select.native.tsx +42 -33
- package/src/Select/Select.styles.old.tsx +353 -0
- package/src/Select/Select.styles.tsx +223 -292
- package/src/Skeleton/Skeleton.styles.old.tsx +67 -0
- package/src/Skeleton/Skeleton.styles.tsx +29 -53
- package/src/Slider/Slider.styles.old.tsx +259 -0
- package/src/Slider/Slider.styles.tsx +153 -234
- package/src/Switch/Switch.native.tsx +7 -5
- package/src/Switch/Switch.styles.old.tsx +203 -0
- package/src/Switch/Switch.styles.tsx +101 -174
- package/src/Switch/Switch.web.tsx +5 -5
- package/src/TabBar/TabBar.native.tsx +3 -2
- package/src/TabBar/TabBar.styles.old.tsx +343 -0
- package/src/TabBar/TabBar.styles.tsx +145 -279
- package/src/Table/Table.native.tsx +18 -9
- package/src/Table/Table.styles.old.tsx +311 -0
- package/src/Table/Table.styles.tsx +152 -286
- package/src/Text/Text.native.tsx +1 -3
- package/src/Text/Text.style.demo.tsx +16 -0
- package/src/Text/Text.styles.old.tsx +219 -0
- package/src/Text/Text.styles.tsx +94 -84
- package/src/Text/Text.web.tsx +2 -2
- package/src/Text/index.ts +1 -0
- package/src/TextArea/TextArea.styles.old.tsx +213 -0
- package/src/TextArea/TextArea.styles.tsx +93 -181
- package/src/Tooltip/Tooltip.styles.old.tsx +82 -0
- package/src/Tooltip/Tooltip.styles.tsx +32 -56
- package/src/Video/Video.styles.old.tsx +51 -0
- package/src/Video/Video.styles.tsx +32 -44
- package/src/View/View.native.tsx +12 -14
- package/src/View/View.styles.old.tsx +125 -0
- package/src/View/View.styles.tsx +76 -106
- package/src/View/View.web.tsx +1 -0
- package/src/examples/CardExamples.tsx +0 -6
- package/src/extensions/extendComponent.ts +61 -0
package/src/Icon/Icon.web.tsx
CHANGED
|
@@ -3,14 +3,16 @@ import MdiIcon from '@mdi/react';
|
|
|
3
3
|
import { IconProps } from './types';
|
|
4
4
|
import { iconStyles } from './Icon.styles';
|
|
5
5
|
import { getWebProps } from 'react-native-unistyles/web';
|
|
6
|
+
import { useUnistyles } from 'react-native-unistyles';
|
|
6
7
|
import useMergeRefs from '../hooks/useMergeRefs';
|
|
8
|
+
import { getColorFromString, Intent, Color } from '@idealyst/theme';
|
|
7
9
|
|
|
8
10
|
// Internal props that include the transformed path from Babel plugin
|
|
9
11
|
interface InternalIconProps extends IconProps {
|
|
10
12
|
path?: string; // Added by Babel plugin transformation
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
const Icon = forwardRef<
|
|
15
|
+
const Icon = forwardRef<HTMLSpanElement, IconProps>((props: InternalIconProps, ref) => {
|
|
14
16
|
const {
|
|
15
17
|
name,
|
|
16
18
|
size = 'md',
|
|
@@ -23,26 +25,60 @@ const Icon = forwardRef<HTMLDivElement, IconProps>((props: InternalIconProps, re
|
|
|
23
25
|
...restProps
|
|
24
26
|
} = props;
|
|
25
27
|
|
|
28
|
+
const { theme } = useUnistyles();
|
|
29
|
+
|
|
26
30
|
// Check if we have a path prop (from Babel plugin transformation)
|
|
27
31
|
const { path } = restProps as { path?: string };
|
|
28
|
-
|
|
32
|
+
|
|
33
|
+
// Compute size from theme
|
|
34
|
+
let iconSize: number;
|
|
35
|
+
if (typeof size === 'number') {
|
|
36
|
+
iconSize = size;
|
|
37
|
+
} else {
|
|
38
|
+
const themeSize = theme.sizes.icon[size as keyof typeof theme.sizes.icon];
|
|
39
|
+
iconSize = typeof themeSize === 'number' ? themeSize : (themeSize?.width ?? 24);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Compute color from intent or color prop or default
|
|
43
|
+
const iconColor = intent
|
|
44
|
+
? theme.intents[intent as Intent]?.primary
|
|
45
|
+
: color
|
|
46
|
+
? getColorFromString(theme, color as Color)
|
|
47
|
+
: theme.colors.text.primary;
|
|
48
|
+
|
|
49
|
+
// Use getWebProps for className generation but override with computed values
|
|
50
|
+
const iconStyle = (iconStyles.icon as any)({ intent, color, size });
|
|
51
|
+
const iconProps = getWebProps([iconStyle, style]);
|
|
29
52
|
|
|
30
53
|
const mergedRef = useMergeRefs(ref, iconProps.ref);
|
|
31
54
|
|
|
32
55
|
// Use MDI React icon when path is provided (transformed by Babel plugin)
|
|
33
56
|
return (
|
|
34
|
-
<
|
|
57
|
+
<span
|
|
35
58
|
{...iconProps}
|
|
36
59
|
ref={mergedRef}
|
|
37
|
-
id={id}
|
|
60
|
+
id={id}
|
|
61
|
+
style={{
|
|
62
|
+
...iconProps.style,
|
|
63
|
+
fontSize: iconSize,
|
|
64
|
+
width: '1em',
|
|
65
|
+
height: '1em',
|
|
66
|
+
display: 'inline-flex',
|
|
67
|
+
alignItems: 'center',
|
|
68
|
+
justifyContent: 'center',
|
|
69
|
+
flexShrink: 0,
|
|
70
|
+
lineHeight: 1,
|
|
71
|
+
color: iconColor,
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
38
74
|
<MdiIcon
|
|
39
75
|
path={path}
|
|
40
|
-
size=
|
|
41
|
-
color=
|
|
76
|
+
size="1em"
|
|
77
|
+
color="currentColor"
|
|
42
78
|
data-testid={testID}
|
|
43
79
|
aria-label={accessibilityLabel || name}
|
|
44
80
|
/>
|
|
45
|
-
</
|
|
81
|
+
</span>
|
|
46
82
|
);
|
|
47
83
|
});
|
|
48
84
|
|
|
@@ -19,6 +19,7 @@ interface IconSvgProps {
|
|
|
19
19
|
|
|
20
20
|
export const IconSvg: React.FC<IconSvgProps> = ({
|
|
21
21
|
path,
|
|
22
|
+
size = '1em',
|
|
22
23
|
color = 'currentColor',
|
|
23
24
|
style,
|
|
24
25
|
'aria-label': ariaLabel,
|
|
@@ -29,6 +30,7 @@ export const IconSvg: React.FC<IconSvgProps> = ({
|
|
|
29
30
|
<MdiIcon
|
|
30
31
|
style={style}
|
|
31
32
|
path={path}
|
|
33
|
+
size={size}
|
|
32
34
|
color={color}
|
|
33
35
|
aria-label={ariaLabel}
|
|
34
36
|
data-testid={testID}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
2
|
+
import { Theme, StylesheetStyles} from '@idealyst/theme';
|
|
3
|
+
import { applyExtensions } from '../extensions/applyExtension';
|
|
4
|
+
|
|
5
|
+
type ImageVariants = {}
|
|
6
|
+
|
|
7
|
+
export type ExpandedImageStyles = StylesheetStyles<keyof ImageVariants>;
|
|
8
|
+
|
|
9
|
+
export type ImageStylesheet = {
|
|
10
|
+
container: ExpandedImageStyles;
|
|
11
|
+
image: ExpandedImageStyles;
|
|
12
|
+
placeholder: ExpandedImageStyles;
|
|
13
|
+
fallback: ExpandedImageStyles;
|
|
14
|
+
loadingIndicator: ExpandedImageStyles;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Style creators for extension support
|
|
18
|
+
function createContainerStyles(theme: Theme) {
|
|
19
|
+
return () => ({
|
|
20
|
+
position: 'relative' as const,
|
|
21
|
+
overflow: 'hidden' as const,
|
|
22
|
+
backgroundColor: theme.colors['gray.200'],
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createImageStyles() {
|
|
27
|
+
return () => ({
|
|
28
|
+
width: '100%' as const,
|
|
29
|
+
height: '100%' as const,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Styles are inlined here instead of in @idealyst/theme because Unistyles' Babel
|
|
34
|
+
// transform on native cannot resolve function calls to extract variant structures.
|
|
35
|
+
// @ts-ignore - TS language server needs restart to pick up theme structure changes
|
|
36
|
+
export const imageStyles = StyleSheet.create((theme: Theme) => {
|
|
37
|
+
// Apply extensions to main visual elements
|
|
38
|
+
|
|
39
|
+
return applyExtensions('Image', theme, {container: createContainerStyles(theme),
|
|
40
|
+
image: createImageStyles(),
|
|
41
|
+
// Additional styles (merged from return)
|
|
42
|
+
// Minor utility styles (not extended)
|
|
43
|
+
placeholder: {
|
|
44
|
+
position: 'absolute',
|
|
45
|
+
top: 0,
|
|
46
|
+
left: 0,
|
|
47
|
+
right: 0,
|
|
48
|
+
bottom: 0,
|
|
49
|
+
display: 'flex',
|
|
50
|
+
alignItems: 'center',
|
|
51
|
+
justifyContent: 'center',
|
|
52
|
+
backgroundColor: theme.colors['gray.200'],
|
|
53
|
+
},
|
|
54
|
+
fallback: {
|
|
55
|
+
position: 'absolute',
|
|
56
|
+
top: 0,
|
|
57
|
+
left: 0,
|
|
58
|
+
right: 0,
|
|
59
|
+
bottom: 0,
|
|
60
|
+
display: 'flex',
|
|
61
|
+
alignItems: 'center',
|
|
62
|
+
justifyContent: 'center',
|
|
63
|
+
backgroundColor: theme.colors['gray.300'],
|
|
64
|
+
color: theme.colors['gray.600'],
|
|
65
|
+
},
|
|
66
|
+
loadingIndicator: {
|
|
67
|
+
color: theme.colors['gray.600'],
|
|
68
|
+
}});
|
|
69
|
+
});
|
|
@@ -1,73 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image styles using defineStyle.
|
|
3
|
+
*/
|
|
1
4
|
import { StyleSheet } from 'react-native-unistyles';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
5
|
+
import { defineStyle, ThemeStyleWrapper } from '@idealyst/theme';
|
|
6
|
+
import type { Theme as BaseTheme } from '@idealyst/theme';
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
// Required: Unistyles must see StyleSheet usage in original source to process this file
|
|
9
|
+
void StyleSheet;
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
// Wrap theme for $iterator support
|
|
12
|
+
type Theme = ThemeStyleWrapper<BaseTheme>;
|
|
8
13
|
|
|
9
|
-
export type
|
|
10
|
-
container: ExpandedImageStyles;
|
|
11
|
-
image: ExpandedImageStyles;
|
|
12
|
-
placeholder: ExpandedImageStyles;
|
|
13
|
-
fallback: ExpandedImageStyles;
|
|
14
|
-
loadingIndicator: ExpandedImageStyles;
|
|
15
|
-
}
|
|
14
|
+
export type ImageDynamicProps = {};
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Image styles - simple container and image styles.
|
|
18
|
+
*/
|
|
19
|
+
export const imageStyles = defineStyle('Image', (theme: Theme) => ({
|
|
20
|
+
container: (_props: ImageDynamicProps) => ({
|
|
20
21
|
position: 'relative' as const,
|
|
21
22
|
overflow: 'hidden' as const,
|
|
22
23
|
backgroundColor: theme.colors['gray.200'],
|
|
23
|
-
})
|
|
24
|
-
}
|
|
24
|
+
}),
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
return () => ({
|
|
26
|
+
image: (_props: ImageDynamicProps) => ({
|
|
28
27
|
width: '100%' as const,
|
|
29
28
|
height: '100%' as const,
|
|
30
|
-
})
|
|
31
|
-
}
|
|
29
|
+
}),
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
31
|
+
placeholder: (_props: ImageDynamicProps) => ({
|
|
32
|
+
position: 'absolute' as const,
|
|
33
|
+
top: 0,
|
|
34
|
+
left: 0,
|
|
35
|
+
right: 0,
|
|
36
|
+
bottom: 0,
|
|
37
|
+
display: 'flex' as const,
|
|
38
|
+
alignItems: 'center' as const,
|
|
39
|
+
justifyContent: 'center' as const,
|
|
40
|
+
backgroundColor: theme.colors['gray.200'],
|
|
41
|
+
}),
|
|
42
|
+
|
|
43
|
+
fallback: (_props: ImageDynamicProps) => ({
|
|
44
|
+
position: 'absolute' as const,
|
|
45
|
+
top: 0,
|
|
46
|
+
left: 0,
|
|
47
|
+
right: 0,
|
|
48
|
+
bottom: 0,
|
|
49
|
+
display: 'flex' as const,
|
|
50
|
+
alignItems: 'center' as const,
|
|
51
|
+
justifyContent: 'center' as const,
|
|
52
|
+
backgroundColor: theme.colors['gray.300'],
|
|
53
|
+
color: theme.colors['gray.600'],
|
|
54
|
+
}),
|
|
42
55
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
position: 'absolute',
|
|
48
|
-
top: 0,
|
|
49
|
-
left: 0,
|
|
50
|
-
right: 0,
|
|
51
|
-
bottom: 0,
|
|
52
|
-
display: 'flex',
|
|
53
|
-
alignItems: 'center',
|
|
54
|
-
justifyContent: 'center',
|
|
55
|
-
backgroundColor: theme.colors['gray.200'],
|
|
56
|
-
},
|
|
57
|
-
fallback: {
|
|
58
|
-
position: 'absolute',
|
|
59
|
-
top: 0,
|
|
60
|
-
left: 0,
|
|
61
|
-
right: 0,
|
|
62
|
-
bottom: 0,
|
|
63
|
-
display: 'flex',
|
|
64
|
-
alignItems: 'center',
|
|
65
|
-
justifyContent: 'center',
|
|
66
|
-
backgroundColor: theme.colors['gray.300'],
|
|
67
|
-
color: theme.colors['gray.600'],
|
|
68
|
-
},
|
|
69
|
-
loadingIndicator: {
|
|
70
|
-
color: theme.colors['gray.600'],
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
});
|
|
56
|
+
loadingIndicator: (_props: ImageDynamicProps) => ({
|
|
57
|
+
color: theme.colors['gray.600'],
|
|
58
|
+
}),
|
|
59
|
+
}));
|
|
@@ -1,10 +1,55 @@
|
|
|
1
|
-
import React, { useState, isValidElement, useMemo } from 'react';
|
|
2
|
-
import { View, TextInput, TouchableOpacity } from 'react-native';
|
|
1
|
+
import React, { useState, isValidElement, useMemo, useEffect, useRef, useCallback } from 'react';
|
|
2
|
+
import { View, TextInput, TouchableOpacity, Platform, TextInputProps } from 'react-native';
|
|
3
3
|
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
|
4
|
+
import { useUnistyles } from 'react-native-unistyles';
|
|
4
5
|
import { InputProps } from './types';
|
|
5
6
|
import { inputStyles } from './Input.styles';
|
|
6
7
|
import { getNativeFormAccessibilityProps } from '../utils/accessibility';
|
|
7
8
|
|
|
9
|
+
// Inner TextInput component that can be memoized to prevent re-renders
|
|
10
|
+
// for Android secure text entry
|
|
11
|
+
type InnerTextInputProps = {
|
|
12
|
+
inputRef: React.ForwardedRef<TextInput>;
|
|
13
|
+
value: string | undefined;
|
|
14
|
+
onChangeText: ((text: string) => void) | undefined;
|
|
15
|
+
isAndroidSecure: boolean;
|
|
16
|
+
textInputProps: Omit<TextInputProps, 'value' | 'defaultValue' | 'onChangeText'>;
|
|
17
|
+
inputStyle: any;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const InnerTextInput = React.memo<InnerTextInputProps>(
|
|
21
|
+
({ inputRef, value, onChangeText, isAndroidSecure, textInputProps, inputStyle }) => {
|
|
22
|
+
return (
|
|
23
|
+
<TextInput
|
|
24
|
+
ref={inputRef}
|
|
25
|
+
// For Android secure text entry, don't pass value prop at all
|
|
26
|
+
// Let TextInput manage its own state to preserve character reveal animation
|
|
27
|
+
{...(isAndroidSecure ? {} : { value })}
|
|
28
|
+
onChangeText={onChangeText}
|
|
29
|
+
style={inputStyle}
|
|
30
|
+
{...textInputProps}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
},
|
|
34
|
+
(prevProps, nextProps) => {
|
|
35
|
+
// For Android secure text entry, skip re-renders when only value changes
|
|
36
|
+
if (nextProps.isAndroidSecure) {
|
|
37
|
+
// Only re-render if non-value props change
|
|
38
|
+
const valueChanged = prevProps.value !== nextProps.value;
|
|
39
|
+
const otherPropsChanged =
|
|
40
|
+
prevProps.onChangeText !== nextProps.onChangeText ||
|
|
41
|
+
prevProps.isAndroidSecure !== nextProps.isAndroidSecure ||
|
|
42
|
+
prevProps.textInputProps !== nextProps.textInputProps ||
|
|
43
|
+
prevProps.inputStyle !== nextProps.inputStyle;
|
|
44
|
+
|
|
45
|
+
if (valueChanged && !otherPropsChanged) {
|
|
46
|
+
return true; // Skip re-render
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return false; // Allow re-render
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
|
|
8
53
|
const Input = React.forwardRef<TextInput, InputProps>(({
|
|
9
54
|
value,
|
|
10
55
|
onChangeText,
|
|
@@ -41,11 +86,30 @@ const Input = React.forwardRef<TextInput, InputProps>(({
|
|
|
41
86
|
const [isFocused, setIsFocused] = useState(false);
|
|
42
87
|
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
|
43
88
|
|
|
89
|
+
// Track if this is a secure field that needs Android workaround
|
|
90
|
+
const isSecureField = inputType === 'password' || secureTextEntry;
|
|
91
|
+
const needsAndroidSecureWorkaround = Platform.OS === 'android' && isSecureField && !isPasswordVisible;
|
|
92
|
+
|
|
93
|
+
// For Android secure text entry, we use an internal ref to track value
|
|
94
|
+
const internalValueRef = useRef(value ?? '');
|
|
95
|
+
|
|
96
|
+
// Sync external value changes to internal ref (for programmatic updates)
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (value !== undefined) {
|
|
99
|
+
internalValueRef.current = value;
|
|
100
|
+
}
|
|
101
|
+
}, [value]);
|
|
102
|
+
|
|
103
|
+
// Get theme for icon sizes and colors
|
|
104
|
+
const { theme } = useUnistyles();
|
|
105
|
+
const iconSize = theme.sizes.input[size].iconSize;
|
|
106
|
+
const iconColor = theme.colors.text.secondary;
|
|
107
|
+
|
|
44
108
|
// Determine if we should show password toggle
|
|
45
109
|
const isPasswordField = inputType === 'password' || secureTextEntry;
|
|
46
110
|
const shouldShowPasswordToggle = isPasswordField && (showPasswordToggle !== false);
|
|
47
111
|
|
|
48
|
-
const getKeyboardType = () => {
|
|
112
|
+
const getKeyboardType = useCallback((): 'default' | 'email-address' | 'numeric' => {
|
|
49
113
|
switch (inputType) {
|
|
50
114
|
case 'email':
|
|
51
115
|
return 'email-address';
|
|
@@ -56,42 +120,34 @@ const Input = React.forwardRef<TextInput, InputProps>(({
|
|
|
56
120
|
default:
|
|
57
121
|
return 'default';
|
|
58
122
|
}
|
|
59
|
-
};
|
|
123
|
+
}, [inputType]);
|
|
60
124
|
|
|
61
|
-
const handleFocus = () => {
|
|
125
|
+
const handleFocus = useCallback(() => {
|
|
62
126
|
setIsFocused(true);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
};
|
|
127
|
+
onFocus?.();
|
|
128
|
+
}, [onFocus]);
|
|
67
129
|
|
|
68
|
-
const handlePress = () => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
}
|
|
130
|
+
const handlePress = useCallback(() => {
|
|
131
|
+
onPress?.();
|
|
132
|
+
}, [onPress]);
|
|
73
133
|
|
|
74
|
-
const handleBlur = () => {
|
|
134
|
+
const handleBlur = useCallback(() => {
|
|
75
135
|
setIsFocused(false);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
};
|
|
136
|
+
onBlur?.();
|
|
137
|
+
}, [onBlur]);
|
|
80
138
|
|
|
81
139
|
const togglePasswordVisibility = () => {
|
|
82
140
|
setIsPasswordVisible(!isPasswordVisible);
|
|
83
141
|
};
|
|
84
142
|
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
marginHorizontal,
|
|
91
|
-
});
|
|
143
|
+
// Memoized change handler for InnerTextInput
|
|
144
|
+
const handleChangeText = useCallback((text: string) => {
|
|
145
|
+
internalValueRef.current = text;
|
|
146
|
+
onChangeText?.(text);
|
|
147
|
+
}, [onChangeText]);
|
|
92
148
|
|
|
93
|
-
//
|
|
94
|
-
const
|
|
149
|
+
// Memoized input style
|
|
150
|
+
const inputStyle = useMemo(() => (inputStyles.input as any)({}), []);
|
|
95
151
|
|
|
96
152
|
// Generate native accessibility props
|
|
97
153
|
const nativeA11yProps = useMemo(() => {
|
|
@@ -119,17 +175,55 @@ const Input = React.forwardRef<TextInput, InputProps>(({
|
|
|
119
175
|
hasError,
|
|
120
176
|
]);
|
|
121
177
|
|
|
178
|
+
// Memoized TextInput props (everything except value/onChangeText)
|
|
179
|
+
const textInputProps = useMemo(() => ({
|
|
180
|
+
onPress: handlePress,
|
|
181
|
+
placeholder,
|
|
182
|
+
editable: !disabled,
|
|
183
|
+
keyboardType: getKeyboardType(),
|
|
184
|
+
secureTextEntry: isSecureField && !isPasswordVisible,
|
|
185
|
+
autoCapitalize,
|
|
186
|
+
onFocus: handleFocus,
|
|
187
|
+
onBlur: handleBlur,
|
|
188
|
+
placeholderTextColor: '#999999',
|
|
189
|
+
...nativeA11yProps,
|
|
190
|
+
}), [
|
|
191
|
+
handlePress,
|
|
192
|
+
placeholder,
|
|
193
|
+
disabled,
|
|
194
|
+
getKeyboardType,
|
|
195
|
+
isSecureField,
|
|
196
|
+
isPasswordVisible,
|
|
197
|
+
autoCapitalize,
|
|
198
|
+
handleFocus,
|
|
199
|
+
handleBlur,
|
|
200
|
+
nativeA11yProps,
|
|
201
|
+
]);
|
|
202
|
+
|
|
203
|
+
// Apply variants to the stylesheet (for size and spacing)
|
|
204
|
+
inputStyles.useVariants({
|
|
205
|
+
size,
|
|
206
|
+
margin,
|
|
207
|
+
marginVertical,
|
|
208
|
+
marginHorizontal,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Compute dynamic styles - call as functions for theme reactivity
|
|
212
|
+
const containerStyle = (inputStyles.container as any)({ type, focused: isFocused, hasError, disabled });
|
|
213
|
+
const leftIconContainerStyle = (inputStyles.leftIconContainer as any)({});
|
|
214
|
+
const rightIconContainerStyle = (inputStyles.rightIconContainer as any)({});
|
|
215
|
+
const passwordToggleStyle = (inputStyles.passwordToggle as any)({});
|
|
216
|
+
|
|
122
217
|
// Helper to render left icon
|
|
123
218
|
const renderLeftIcon = () => {
|
|
124
219
|
if (!leftIcon) return null;
|
|
125
220
|
|
|
126
221
|
if (typeof leftIcon === 'string') {
|
|
127
|
-
const iconStyle = inputStyles.leftIcon;
|
|
128
222
|
return (
|
|
129
223
|
<MaterialCommunityIcons
|
|
130
224
|
name={leftIcon}
|
|
131
|
-
size={
|
|
132
|
-
color={
|
|
225
|
+
size={iconSize}
|
|
226
|
+
color={iconColor}
|
|
133
227
|
/>
|
|
134
228
|
);
|
|
135
229
|
} else if (isValidElement(leftIcon)) {
|
|
@@ -144,12 +238,11 @@ const Input = React.forwardRef<TextInput, InputProps>(({
|
|
|
144
238
|
if (!rightIcon) return null;
|
|
145
239
|
|
|
146
240
|
if (typeof rightIcon === 'string') {
|
|
147
|
-
const iconStyle = inputStyles.rightIcon;
|
|
148
241
|
return (
|
|
149
242
|
<MaterialCommunityIcons
|
|
150
243
|
name={rightIcon}
|
|
151
|
-
size={
|
|
152
|
-
color={
|
|
244
|
+
size={iconSize}
|
|
245
|
+
color={iconColor}
|
|
153
246
|
/>
|
|
154
247
|
);
|
|
155
248
|
} else if (isValidElement(rightIcon)) {
|
|
@@ -163,45 +256,37 @@ const Input = React.forwardRef<TextInput, InputProps>(({
|
|
|
163
256
|
<View style={[containerStyle, style]} testID={testID} nativeID={id}>
|
|
164
257
|
{/* Left Icon */}
|
|
165
258
|
{leftIcon && (
|
|
166
|
-
<View style={
|
|
259
|
+
<View style={leftIconContainerStyle}>
|
|
167
260
|
{renderLeftIcon()}
|
|
168
261
|
</View>
|
|
169
262
|
)}
|
|
170
263
|
|
|
171
264
|
{/* Input */}
|
|
172
|
-
<
|
|
173
|
-
|
|
174
|
-
ref={ref}
|
|
265
|
+
<InnerTextInput
|
|
266
|
+
inputRef={ref}
|
|
175
267
|
value={value}
|
|
176
|
-
onChangeText={
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
secureTextEntry={(secureTextEntry || inputType === 'password') && !isPasswordVisible}
|
|
181
|
-
autoCapitalize={autoCapitalize}
|
|
182
|
-
onFocus={handleFocus}
|
|
183
|
-
onBlur={handleBlur}
|
|
184
|
-
style={(inputStyles.input as any)({})}
|
|
185
|
-
placeholderTextColor="#999999"
|
|
186
|
-
{...nativeA11yProps}
|
|
268
|
+
onChangeText={handleChangeText}
|
|
269
|
+
isAndroidSecure={needsAndroidSecureWorkaround}
|
|
270
|
+
inputStyle={inputStyle}
|
|
271
|
+
textInputProps={textInputProps}
|
|
187
272
|
/>
|
|
188
273
|
|
|
189
274
|
{/* Right Icon or Password Toggle */}
|
|
190
275
|
{shouldShowPasswordToggle ? (
|
|
191
276
|
<TouchableOpacity
|
|
192
|
-
style={
|
|
277
|
+
style={passwordToggleStyle}
|
|
193
278
|
onPress={togglePasswordVisibility}
|
|
194
279
|
disabled={disabled}
|
|
195
280
|
accessibilityLabel={isPasswordVisible ? 'Hide password' : 'Show password'}
|
|
196
281
|
>
|
|
197
282
|
<MaterialCommunityIcons
|
|
198
283
|
name={isPasswordVisible ? 'eye-off' : 'eye'}
|
|
199
|
-
size={
|
|
200
|
-
color={
|
|
284
|
+
size={iconSize}
|
|
285
|
+
color={iconColor}
|
|
201
286
|
/>
|
|
202
287
|
</TouchableOpacity>
|
|
203
288
|
) : rightIcon ? (
|
|
204
|
-
<View style={
|
|
289
|
+
<View style={rightIconContainerStyle}>
|
|
205
290
|
{renderRightIcon()}
|
|
206
291
|
</View>
|
|
207
292
|
) : null}
|