@idealyst/components 1.2.29 → 1.2.31
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 +4 -4
- package/plugin/__tests__/web.test.ts +2 -2
- package/plugin/web.js +2 -0
- package/src/Accordion/Accordion.native.tsx +3 -2
- package/src/ActivityIndicator/ActivityIndicator.native.tsx +4 -2
- package/src/ActivityIndicator/ActivityIndicator.styles.tsx +22 -27
- package/src/ActivityIndicator/ActivityIndicator.web.tsx +17 -29
- package/src/Alert/Alert.native.tsx +20 -10
- package/src/Alert/Alert.styles.tsx +173 -86
- package/src/Alert/Alert.web.tsx +34 -30
- 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.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/IconButton/IconButton.native.tsx +219 -0
- package/src/IconButton/IconButton.styles.tsx +127 -0
- package/src/IconButton/IconButton.web.tsx +198 -0
- package/src/IconButton/index.native.ts +5 -0
- package/src/IconButton/index.ts +5 -0
- package/src/IconButton/index.web.ts +5 -0
- package/src/IconButton/types.ts +84 -0
- 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/Skeleton/Skeleton.web.tsx +1 -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 +52 -17
- package/src/Switch/Switch.web.tsx +15 -16
- 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.styles.tsx +1 -0
- package/src/View/View.web.tsx +9 -2
- package/src/examples/ActivityIndicatorExamples.tsx +177 -0
- 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 +26 -26
- package/src/examples/TableExamples.tsx +1 -1
- package/src/examples/TooltipExamples.tsx +2 -2
- package/src/examples/index.ts +1 -0
- package/src/extensions/index.ts +1 -0
- package/src/extensions/types.ts +22 -3
- package/src/index.native.ts +4 -0
- package/src/index.ts +27 -2
- package/src/utils/index.ts +12 -0
- package/src/utils/refTypes.ts +50 -0
|
@@ -48,6 +48,12 @@ export const switchStyles = defineStyle('Switch', (theme: Theme) => ({
|
|
|
48
48
|
|
|
49
49
|
switchContainer: (_props: SwitchDynamicProps) => ({
|
|
50
50
|
justifyContent: 'center' as const,
|
|
51
|
+
variants: {
|
|
52
|
+
disabled: {
|
|
53
|
+
true: { _web: { cursor: 'not-allowed' } },
|
|
54
|
+
false: { _web: { cursor: 'pointer' } },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
51
57
|
_web: {
|
|
52
58
|
border: 'none',
|
|
53
59
|
padding: 0,
|
|
@@ -56,29 +62,41 @@ export const switchStyles = defineStyle('Switch', (theme: Theme) => ({
|
|
|
56
62
|
},
|
|
57
63
|
}),
|
|
58
64
|
|
|
59
|
-
switchTrack: (
|
|
65
|
+
switchTrack: (_props: SwitchDynamicProps) => ({
|
|
60
66
|
borderRadius: 9999,
|
|
61
67
|
position: 'relative' as const,
|
|
62
|
-
|
|
63
|
-
? theme.intents[intent].primary
|
|
64
|
-
: theme.colors.border.secondary,
|
|
65
|
-
opacity: disabled ? 0.5 : 1,
|
|
68
|
+
pointerEvents: 'none' as const,
|
|
66
69
|
variants: {
|
|
67
70
|
size: {
|
|
68
71
|
width: theme.sizes.$switch.trackWidth,
|
|
69
72
|
height: theme.sizes.$switch.trackHeight,
|
|
70
73
|
},
|
|
74
|
+
checked: {
|
|
75
|
+
false: {
|
|
76
|
+
backgroundColor: theme.colors.border.secondary,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
disabled: {
|
|
80
|
+
true: { opacity: 0.5 },
|
|
81
|
+
false: { opacity: 1 },
|
|
82
|
+
},
|
|
71
83
|
},
|
|
84
|
+
compoundVariants: [
|
|
85
|
+
{
|
|
86
|
+
checked: true,
|
|
87
|
+
styles: {
|
|
88
|
+
backgroundColor: theme.$intents.primary,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
],
|
|
72
92
|
_web: {
|
|
73
93
|
transition: 'background-color 0.2s ease',
|
|
74
|
-
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
75
|
-
_hover: disabled ? {} : { opacity: 0.9 },
|
|
76
|
-
_active: disabled ? {} : { opacity: 0.8 },
|
|
77
94
|
},
|
|
78
95
|
}),
|
|
79
96
|
|
|
80
97
|
switchThumb: ({ size = 'md', checked = false }: SwitchDynamicProps) => {
|
|
81
|
-
|
|
98
|
+
// translateX needs runtime calculation based on size
|
|
99
|
+
const sizeValue = theme.sizes.switch[size] ?? theme.sizes.switch.md;
|
|
82
100
|
const translateX = checked ? sizeValue.translateX : 0;
|
|
83
101
|
|
|
84
102
|
return {
|
|
@@ -109,26 +127,43 @@ export const switchStyles = defineStyle('Switch', (theme: Theme) => ({
|
|
|
109
127
|
} as const;
|
|
110
128
|
},
|
|
111
129
|
|
|
112
|
-
thumbIcon: (
|
|
130
|
+
thumbIcon: (_props: SwitchDynamicProps) => ({
|
|
113
131
|
display: 'flex' as const,
|
|
114
132
|
alignItems: 'center' as const,
|
|
115
133
|
justifyContent: 'center' as const,
|
|
116
|
-
color: checked
|
|
117
|
-
? theme.intents[intent].primary
|
|
118
|
-
: theme.colors.border.secondary,
|
|
119
134
|
variants: {
|
|
120
135
|
size: {
|
|
121
136
|
width: theme.sizes.$switch.thumbIconSize,
|
|
122
137
|
height: theme.sizes.$switch.thumbIconSize,
|
|
123
138
|
},
|
|
139
|
+
checked: {
|
|
140
|
+
false: {
|
|
141
|
+
color: theme.colors.border.secondary,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
124
144
|
},
|
|
145
|
+
compoundVariants: [
|
|
146
|
+
{
|
|
147
|
+
checked: true,
|
|
148
|
+
styles: {
|
|
149
|
+
color: theme.$intents.primary,
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
],
|
|
125
153
|
}),
|
|
126
154
|
|
|
127
|
-
label: (
|
|
155
|
+
label: (_props: SwitchDynamicProps) => ({
|
|
128
156
|
fontSize: 14,
|
|
129
157
|
color: theme.colors.text.primary,
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
158
|
+
variants: {
|
|
159
|
+
disabled: {
|
|
160
|
+
true: { opacity: 0.5 },
|
|
161
|
+
false: { opacity: 1 },
|
|
162
|
+
},
|
|
163
|
+
labelPosition: {
|
|
164
|
+
left: { marginRight: 8, marginLeft: 0 },
|
|
165
|
+
right: { marginLeft: 8, marginRight: 0 },
|
|
166
|
+
},
|
|
167
|
+
},
|
|
133
168
|
}),
|
|
134
169
|
}));
|
|
@@ -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
|
|
|
@@ -76,12 +77,15 @@ const Switch = forwardRef<HTMLDivElement | HTMLButtonElement, SwitchProps>(({
|
|
|
76
77
|
checked,
|
|
77
78
|
]);
|
|
78
79
|
|
|
80
|
+
|
|
79
81
|
// Apply variants using the correct Unistyles v3 pattern
|
|
80
82
|
switchStyles.useVariants({
|
|
81
|
-
size
|
|
83
|
+
size,
|
|
84
|
+
checked,
|
|
82
85
|
disabled: disabled as boolean,
|
|
83
|
-
|
|
86
|
+
labelPosition,
|
|
84
87
|
margin,
|
|
88
|
+
intent,
|
|
85
89
|
marginVertical,
|
|
86
90
|
marginHorizontal,
|
|
87
91
|
});
|
|
@@ -93,7 +97,7 @@ const Switch = forwardRef<HTMLDivElement | HTMLButtonElement, SwitchProps>(({
|
|
|
93
97
|
|
|
94
98
|
// Helper to render icon
|
|
95
99
|
const renderIcon = () => {
|
|
96
|
-
const iconToRender = checked ?
|
|
100
|
+
const iconToRender = checked ? onIcon : offIcon;
|
|
97
101
|
if (!iconToRender) return null;
|
|
98
102
|
|
|
99
103
|
if (isIconName(iconToRender)) {
|
|
@@ -118,13 +122,7 @@ const Switch = forwardRef<HTMLDivElement | HTMLButtonElement, SwitchProps>(({
|
|
|
118
122
|
|
|
119
123
|
// Computed container props with dynamic styles (for when label exists)
|
|
120
124
|
const computedContainerProps = getWebProps([
|
|
121
|
-
(switchStyles.container as any)({}),
|
|
122
|
-
style as any,
|
|
123
|
-
{
|
|
124
|
-
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
125
|
-
display: 'inline-flex',
|
|
126
|
-
alignItems: 'center',
|
|
127
|
-
}
|
|
125
|
+
(switchStyles.container as any)({ disabled }),
|
|
128
126
|
]);
|
|
129
127
|
|
|
130
128
|
const mergedButtonRef = useMergeRefs(ref as React.Ref<HTMLButtonElement>, computedButtonProps.ref);
|
|
@@ -134,6 +132,7 @@ const Switch = forwardRef<HTMLDivElement | HTMLButtonElement, SwitchProps>(({
|
|
|
134
132
|
<button
|
|
135
133
|
{...computedButtonProps}
|
|
136
134
|
{...ariaProps}
|
|
135
|
+
style={style as any}
|
|
137
136
|
ref={mergedButtonRef}
|
|
138
137
|
onClick={handleClick}
|
|
139
138
|
disabled={disabled}
|
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;
|