@boostdev/design-system-components 2.4.1 → 2.5.0
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/AGENTS.md +4 -20
- package/README.md +3 -23
- package/dist/client.cjs +128 -62
- package/dist/client.css +537 -537
- package/dist/client.d.cts +11 -1
- package/dist/client.d.ts +11 -1
- package/dist/client.js +128 -62
- package/dist/index.cjs +128 -62
- package/dist/index.css +537 -537
- package/dist/index.d.cts +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +128 -62
- package/dist/web-components/index.js +36 -4
- package/package.json +2 -10
- package/src/components/layout/Grid/Grid.mdx +20 -2
- package/src/components/layout/Grid/Grid.spec.tsx +206 -4
- package/src/components/layout/Grid/Grid.tsx +15 -1
- package/src/components/layout/Grid/masonry.ts +148 -10
- package/dist/native/index.cjs +0 -4337
- package/dist/native/index.d.cts +0 -574
- package/dist/native/index.d.ts +0 -574
- package/dist/native/index.js +0 -4421
- package/src/components/interaction/Button/Button.native.mdx +0 -91
- package/src/components/interaction/Button/Button.native.spec.tsx +0 -55
- package/src/components/interaction/Button/Button.native.stories.tsx +0 -141
- package/src/components/interaction/Button/Button.native.tsx +0 -105
- package/src/components/interaction/Dialog/Dialog.native.mdx +0 -61
- package/src/components/interaction/Dialog/Dialog.native.spec.tsx +0 -73
- package/src/components/interaction/Dialog/Dialog.native.stories.tsx +0 -53
- package/src/components/interaction/Dialog/Dialog.native.tsx +0 -128
- package/src/components/interaction/Drawer/Drawer.native.mdx +0 -58
- package/src/components/interaction/Drawer/Drawer.native.spec.tsx +0 -81
- package/src/components/interaction/Drawer/Drawer.native.stories.tsx +0 -33
- package/src/components/interaction/Drawer/Drawer.native.tsx +0 -175
- package/src/components/interaction/DropdownMenu/DropdownMenu.native.mdx +0 -74
- package/src/components/interaction/DropdownMenu/DropdownMenu.native.spec.tsx +0 -78
- package/src/components/interaction/DropdownMenu/DropdownMenu.native.stories.tsx +0 -51
- package/src/components/interaction/DropdownMenu/DropdownMenu.native.tsx +0 -254
- package/src/components/interaction/Popover/Popover.native.mdx +0 -61
- package/src/components/interaction/Popover/Popover.native.spec.tsx +0 -73
- package/src/components/interaction/Popover/Popover.native.stories.tsx +0 -44
- package/src/components/interaction/Popover/Popover.native.tsx +0 -87
- package/src/components/interaction/Rating/Rating.native.mdx +0 -55
- package/src/components/interaction/Rating/Rating.native.spec.tsx +0 -38
- package/src/components/interaction/Rating/Rating.native.stories.tsx +0 -37
- package/src/components/interaction/Rating/Rating.native.tsx +0 -50
- package/src/components/interaction/Toast/Toast.native.mdx +0 -81
- package/src/components/interaction/Toast/Toast.native.spec.tsx +0 -80
- package/src/components/interaction/Toast/Toast.native.stories.tsx +0 -47
- package/src/components/interaction/Toast/Toast.native.tsx +0 -202
- package/src/components/interaction/form/Checkbox/Checkbox.native.mdx +0 -74
- package/src/components/interaction/form/Checkbox/Checkbox.native.spec.tsx +0 -42
- package/src/components/interaction/form/Checkbox/Checkbox.native.stories.tsx +0 -34
- package/src/components/interaction/form/Checkbox/Checkbox.native.tsx +0 -80
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.mdx +0 -78
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.spec.tsx +0 -57
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.stories.tsx +0 -96
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.tsx +0 -61
- package/src/components/interaction/form/Combobox/Combobox.native.mdx +0 -86
- package/src/components/interaction/form/Combobox/Combobox.native.spec.tsx +0 -60
- package/src/components/interaction/form/Combobox/Combobox.native.stories.tsx +0 -99
- package/src/components/interaction/form/Combobox/Combobox.native.tsx +0 -211
- package/src/components/interaction/form/FileInput/FileInput.native.mdx +0 -86
- package/src/components/interaction/form/FileInput/FileInput.native.spec.tsx +0 -73
- package/src/components/interaction/form/FileInput/FileInput.native.stories.tsx +0 -69
- package/src/components/interaction/form/FileInput/FileInput.native.tsx +0 -129
- package/src/components/interaction/form/FormInput/FormInput.native.mdx +0 -81
- package/src/components/interaction/form/FormInput/FormInput.native.spec.tsx +0 -51
- package/src/components/interaction/form/FormInput/FormInput.native.stories.tsx +0 -54
- package/src/components/interaction/form/FormInput/FormInput.native.tsx +0 -122
- package/src/components/interaction/form/NumberInput/NumberInput.native.mdx +0 -87
- package/src/components/interaction/form/NumberInput/NumberInput.native.spec.tsx +0 -63
- package/src/components/interaction/form/NumberInput/NumberInput.native.stories.tsx +0 -63
- package/src/components/interaction/form/NumberInput/NumberInput.native.tsx +0 -198
- package/src/components/interaction/form/Radio/Radio.native.spec.tsx +0 -79
- package/src/components/interaction/form/Radio/Radio.native.stories.tsx +0 -44
- package/src/components/interaction/form/Radio/Radio.native.tsx +0 -79
- package/src/components/interaction/form/RadioGroup/RadioGroup.native.mdx +0 -79
- package/src/components/interaction/form/RadioGroup/RadioGroup.native.spec.tsx +0 -66
- package/src/components/interaction/form/RadioGroup/RadioGroup.native.stories.tsx +0 -96
- package/src/components/interaction/form/RadioGroup/RadioGroup.native.tsx +0 -61
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.mdx +0 -78
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.spec.tsx +0 -82
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.stories.tsx +0 -99
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.tsx +0 -121
- package/src/components/interaction/form/Select/Select.native.mdx +0 -85
- package/src/components/interaction/form/Select/Select.native.spec.tsx +0 -64
- package/src/components/interaction/form/Select/Select.native.stories.tsx +0 -75
- package/src/components/interaction/form/Select/Select.native.tsx +0 -252
- package/src/components/interaction/form/Slider/Slider.native.mdx +0 -83
- package/src/components/interaction/form/Slider/Slider.native.spec.tsx +0 -51
- package/src/components/interaction/form/Slider/Slider.native.stories.tsx +0 -58
- package/src/components/interaction/form/Slider/Slider.native.tsx +0 -188
- package/src/components/interaction/form/Switch/Switch.native.spec.tsx +0 -60
- package/src/components/interaction/form/Switch/Switch.native.stories.tsx +0 -33
- package/src/components/interaction/form/Switch/Switch.native.tsx +0 -59
- package/src/components/interaction/form/Textarea/Textarea.native.mdx +0 -76
- package/src/components/interaction/form/Textarea/Textarea.native.spec.tsx +0 -50
- package/src/components/interaction/form/Textarea/Textarea.native.stories.tsx +0 -50
- package/src/components/interaction/form/Textarea/Textarea.native.tsx +0 -108
- package/src/components/interaction/form/atoms/Label.native.spec.tsx +0 -39
- package/src/components/interaction/form/atoms/Label.native.stories.tsx +0 -18
- package/src/components/interaction/form/atoms/Label.native.tsx +0 -29
- package/src/components/interaction/form/atoms/Message.native.spec.tsx +0 -30
- package/src/components/interaction/form/atoms/Message.native.stories.tsx +0 -22
- package/src/components/interaction/form/atoms/Message.native.tsx +0 -34
- package/src/components/layout/ButtonGroup/ButtonGroup.native.spec.tsx +0 -83
- package/src/components/layout/ButtonGroup/ButtonGroup.native.stories.tsx +0 -44
- package/src/components/layout/ButtonGroup/ButtonGroup.native.tsx +0 -44
- package/src/components/layout/Card/Card.native.spec.tsx +0 -102
- package/src/components/layout/Card/Card.native.stories.tsx +0 -51
- package/src/components/layout/Card/Card.native.tsx +0 -89
- package/src/components/layout/IconWrapper/IconWrapper.native.spec.tsx +0 -39
- package/src/components/layout/IconWrapper/IconWrapper.native.stories.tsx +0 -37
- package/src/components/layout/IconWrapper/IconWrapper.native.tsx +0 -20
- package/src/components/layout/SectionHeader/SectionHeader.native.spec.tsx +0 -51
- package/src/components/layout/SectionHeader/SectionHeader.native.stories.tsx +0 -36
- package/src/components/layout/SectionHeader/SectionHeader.native.tsx +0 -79
- package/src/components/ui/Accordion/Accordion.native.mdx +0 -65
- package/src/components/ui/Accordion/Accordion.native.spec.tsx +0 -69
- package/src/components/ui/Accordion/Accordion.native.stories.tsx +0 -52
- package/src/components/ui/Accordion/Accordion.native.tsx +0 -141
- package/src/components/ui/Alert/Alert.native.mdx +0 -62
- package/src/components/ui/Alert/Alert.native.spec.tsx +0 -76
- package/src/components/ui/Alert/Alert.native.stories.tsx +0 -41
- package/src/components/ui/Alert/Alert.native.tsx +0 -94
- package/src/components/ui/Avatar/Avatar.native.mdx +0 -50
- package/src/components/ui/Avatar/Avatar.native.spec.tsx +0 -47
- package/src/components/ui/Avatar/Avatar.native.stories.tsx +0 -32
- package/src/components/ui/Avatar/Avatar.native.tsx +0 -85
- package/src/components/ui/Badge/Badge.native.mdx +0 -54
- package/src/components/ui/Badge/Badge.native.spec.tsx +0 -45
- package/src/components/ui/Badge/Badge.native.stories.tsx +0 -34
- package/src/components/ui/Badge/Badge.native.tsx +0 -50
- package/src/components/ui/Breadcrumb/Breadcrumb.native.mdx +0 -52
- package/src/components/ui/Breadcrumb/Breadcrumb.native.spec.tsx +0 -49
- package/src/components/ui/Breadcrumb/Breadcrumb.native.stories.tsx +0 -46
- package/src/components/ui/Breadcrumb/Breadcrumb.native.tsx +0 -83
- package/src/components/ui/Calendar/Calendar.native.mdx +0 -58
- package/src/components/ui/Calendar/Calendar.native.spec.tsx +0 -92
- package/src/components/ui/Calendar/Calendar.native.stories.tsx +0 -45
- package/src/components/ui/Calendar/Calendar.native.tsx +0 -294
- package/src/components/ui/Carousel/Carousel.native.mdx +0 -55
- package/src/components/ui/Carousel/Carousel.native.spec.tsx +0 -55
- package/src/components/ui/Carousel/Carousel.native.stories.tsx +0 -59
- package/src/components/ui/Carousel/Carousel.native.tsx +0 -184
- package/src/components/ui/Collapsible/Collapsible.native.mdx +0 -53
- package/src/components/ui/Collapsible/Collapsible.native.spec.tsx +0 -68
- package/src/components/ui/Collapsible/Collapsible.native.stories.tsx +0 -48
- package/src/components/ui/Collapsible/Collapsible.native.tsx +0 -139
- package/src/components/ui/DescriptionList/DescriptionList.native.mdx +0 -50
- package/src/components/ui/DescriptionList/DescriptionList.native.spec.tsx +0 -37
- package/src/components/ui/DescriptionList/DescriptionList.native.stories.tsx +0 -48
- package/src/components/ui/DescriptionList/DescriptionList.native.tsx +0 -54
- package/src/components/ui/Link/Link.native.mdx +0 -53
- package/src/components/ui/Link/Link.native.spec.tsx +0 -35
- package/src/components/ui/Link/Link.native.stories.tsx +0 -46
- package/src/components/ui/Link/Link.native.tsx +0 -48
- package/src/components/ui/Loading/Loading.native.spec.tsx +0 -24
- package/src/components/ui/Loading/Loading.native.stories.tsx +0 -31
- package/src/components/ui/Loading/Loading.native.tsx +0 -29
- package/src/components/ui/NotificationBanner/NotificationBanner.native.spec.tsx +0 -85
- package/src/components/ui/NotificationBanner/NotificationBanner.native.stories.tsx +0 -37
- package/src/components/ui/NotificationBanner/NotificationBanner.native.tsx +0 -76
- package/src/components/ui/Pagination/Pagination.native.mdx +0 -62
- package/src/components/ui/Pagination/Pagination.native.spec.tsx +0 -86
- package/src/components/ui/Pagination/Pagination.native.stories.tsx +0 -47
- package/src/components/ui/Pagination/Pagination.native.tsx +0 -157
- package/src/components/ui/Progress/Progress.native.spec.tsx +0 -56
- package/src/components/ui/Progress/Progress.native.stories.tsx +0 -34
- package/src/components/ui/Progress/Progress.native.tsx +0 -84
- package/src/components/ui/ProgressCircle/ProgressCircle.native.mdx +0 -63
- package/src/components/ui/ProgressCircle/ProgressCircle.native.spec.tsx +0 -39
- package/src/components/ui/ProgressCircle/ProgressCircle.native.stories.tsx +0 -36
- package/src/components/ui/ProgressCircle/ProgressCircle.native.tsx +0 -176
- package/src/components/ui/Separator/Separator.native.spec.tsx +0 -39
- package/src/components/ui/Separator/Separator.native.stories.tsx +0 -38
- package/src/components/ui/Separator/Separator.native.tsx +0 -32
- package/src/components/ui/Skeleton/Skeleton.native.spec.tsx +0 -39
- package/src/components/ui/Skeleton/Skeleton.native.stories.tsx +0 -31
- package/src/components/ui/Skeleton/Skeleton.native.tsx +0 -42
- package/src/components/ui/Table/Table.native.mdx +0 -72
- package/src/components/ui/Table/Table.native.spec.tsx +0 -83
- package/src/components/ui/Table/Table.native.stories.tsx +0 -55
- package/src/components/ui/Table/Table.native.tsx +0 -196
- package/src/components/ui/Tabs/Tabs.native.mdx +0 -65
- package/src/components/ui/Tabs/Tabs.native.spec.tsx +0 -65
- package/src/components/ui/Tabs/Tabs.native.stories.tsx +0 -57
- package/src/components/ui/Tabs/Tabs.native.tsx +0 -133
- package/src/components/ui/Tooltip/Tooltip.native.mdx +0 -47
- package/src/components/ui/Tooltip/Tooltip.native.spec.tsx +0 -37
- package/src/components/ui/Tooltip/Tooltip.native.stories.tsx +0 -38
- package/src/components/ui/Tooltip/Tooltip.native.tsx +0 -57
- package/src/components/ui/Typography/Typography.native.mdx +0 -56
- package/src/components/ui/Typography/Typography.native.spec.tsx +0 -49
- package/src/components/ui/Typography/Typography.native.stories.tsx +0 -36
- package/src/components/ui/Typography/Typography.native.tsx +0 -65
- package/src/native/ThemeContext.tsx +0 -28
- package/src/native/tokens.ts +0 -13
- package/src/native.ts +0 -76
- package/src/stories/ReactNative.mdx +0 -156
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import { useState, useMemo } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
StyleSheet,
|
|
4
|
-
View,
|
|
5
|
-
TextInput,
|
|
6
|
-
Pressable,
|
|
7
|
-
Text,
|
|
8
|
-
ScrollView,
|
|
9
|
-
StyleProp,
|
|
10
|
-
ViewStyle,
|
|
11
|
-
} from 'react-native';
|
|
12
|
-
import { spacing, font, border, lineHeight } from '../../../../native/tokens';
|
|
13
|
-
import { useTheme } from '../../../../native/ThemeContext';
|
|
14
|
-
import { Label } from '../atoms/Label.native';
|
|
15
|
-
import { Message } from '../atoms/Message.native';
|
|
16
|
-
|
|
17
|
-
export interface ComboboxOption {
|
|
18
|
-
value: string;
|
|
19
|
-
label: string;
|
|
20
|
-
disabled?: boolean;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface ComboboxProps {
|
|
24
|
-
label: string;
|
|
25
|
-
name: string;
|
|
26
|
-
options: ComboboxOption[];
|
|
27
|
-
placeholder?: string;
|
|
28
|
-
value?: string;
|
|
29
|
-
onChange?: (value: string) => void;
|
|
30
|
-
disabled?: boolean;
|
|
31
|
-
error?: string;
|
|
32
|
-
hint?: string;
|
|
33
|
-
style?: StyleProp<ViewStyle>;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const MAX_DROPDOWN_HEIGHT = 200;
|
|
37
|
-
|
|
38
|
-
const styles = StyleSheet.create({
|
|
39
|
-
container: {
|
|
40
|
-
gap: spacing.xxs,
|
|
41
|
-
},
|
|
42
|
-
inputWrapper: {
|
|
43
|
-
position: 'relative',
|
|
44
|
-
},
|
|
45
|
-
input: {
|
|
46
|
-
fontFamily: font.family.body,
|
|
47
|
-
fontSize: font.size.body,
|
|
48
|
-
lineHeight: lineHeight.body,
|
|
49
|
-
paddingVertical: spacing.xs,
|
|
50
|
-
paddingHorizontal: spacing.s,
|
|
51
|
-
borderWidth: 1,
|
|
52
|
-
borderRadius: border.radius.full,
|
|
53
|
-
minHeight: 44,
|
|
54
|
-
},
|
|
55
|
-
dropdown: {
|
|
56
|
-
position: 'absolute',
|
|
57
|
-
top: '100%',
|
|
58
|
-
start: 0,
|
|
59
|
-
end: 0,
|
|
60
|
-
maxHeight: MAX_DROPDOWN_HEIGHT,
|
|
61
|
-
borderWidth: 1,
|
|
62
|
-
borderTopWidth: 0,
|
|
63
|
-
borderBottomStartRadius: border.radius.xs,
|
|
64
|
-
borderBottomEndRadius: border.radius.xs,
|
|
65
|
-
zIndex: 10,
|
|
66
|
-
},
|
|
67
|
-
option: {
|
|
68
|
-
paddingVertical: spacing.xs,
|
|
69
|
-
paddingHorizontal: spacing.s,
|
|
70
|
-
minHeight: 44,
|
|
71
|
-
justifyContent: 'center',
|
|
72
|
-
},
|
|
73
|
-
optionText: {
|
|
74
|
-
fontFamily: font.family.body,
|
|
75
|
-
fontSize: font.size.body,
|
|
76
|
-
lineHeight: lineHeight.body,
|
|
77
|
-
},
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
export function Combobox({
|
|
81
|
-
label,
|
|
82
|
-
name,
|
|
83
|
-
options,
|
|
84
|
-
placeholder,
|
|
85
|
-
value,
|
|
86
|
-
onChange,
|
|
87
|
-
disabled = false,
|
|
88
|
-
error,
|
|
89
|
-
hint,
|
|
90
|
-
style,
|
|
91
|
-
}: Readonly<ComboboxProps>) {
|
|
92
|
-
const { colors } = useTheme();
|
|
93
|
-
|
|
94
|
-
const selectedOption = useMemo(
|
|
95
|
-
() => options.find((o) => o.value === value),
|
|
96
|
-
[options, value],
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
const [inputValue, setInputValue] = useState(selectedOption?.label ?? '');
|
|
100
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
101
|
-
|
|
102
|
-
const filtered = useMemo(
|
|
103
|
-
() =>
|
|
104
|
-
options.filter((o) =>
|
|
105
|
-
o.label.toLowerCase().includes(inputValue.toLowerCase()),
|
|
106
|
-
),
|
|
107
|
-
[options, inputValue],
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
const selectOption = (option: ComboboxOption) => {
|
|
111
|
-
setInputValue(option.label);
|
|
112
|
-
setIsOpen(false);
|
|
113
|
-
onChange?.(option.value);
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
const handleChangeText = (text: string) => {
|
|
117
|
-
setInputValue(text);
|
|
118
|
-
setIsOpen(true);
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
const handleFocus = () => {
|
|
122
|
-
setIsOpen(true);
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
const handleBlur = () => {
|
|
126
|
-
// Delay closing so onPress on an option can fire first
|
|
127
|
-
setTimeout(() => setIsOpen(false), 150);
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
const borderColor = error ? colors.colorError : colors.colorOnBg;
|
|
131
|
-
|
|
132
|
-
return (
|
|
133
|
-
<View style={[styles.container, style]}>
|
|
134
|
-
<Label label={label} />
|
|
135
|
-
<View style={styles.inputWrapper}>
|
|
136
|
-
<TextInput
|
|
137
|
-
value={inputValue}
|
|
138
|
-
onChangeText={handleChangeText}
|
|
139
|
-
onFocus={handleFocus}
|
|
140
|
-
onBlur={handleBlur}
|
|
141
|
-
placeholder={placeholder}
|
|
142
|
-
placeholderTextColor={colors.colorOnBgSubtle}
|
|
143
|
-
editable={!disabled}
|
|
144
|
-
accessibilityRole="combobox"
|
|
145
|
-
accessibilityLabel={typeof label === 'string' ? label : name}
|
|
146
|
-
accessibilityState={{ disabled, expanded: isOpen }}
|
|
147
|
-
style={[
|
|
148
|
-
styles.input,
|
|
149
|
-
{
|
|
150
|
-
borderColor,
|
|
151
|
-
color: colors.colorOnBg,
|
|
152
|
-
backgroundColor: colors.colorBg,
|
|
153
|
-
},
|
|
154
|
-
disabled && { opacity: 0.4 },
|
|
155
|
-
]}
|
|
156
|
-
/>
|
|
157
|
-
{isOpen && filtered.length > 0 && (
|
|
158
|
-
<ScrollView
|
|
159
|
-
style={[
|
|
160
|
-
styles.dropdown,
|
|
161
|
-
{
|
|
162
|
-
borderColor: colors.colorBgSubtle,
|
|
163
|
-
backgroundColor: colors.colorBg,
|
|
164
|
-
},
|
|
165
|
-
]}
|
|
166
|
-
keyboardShouldPersistTaps="handled"
|
|
167
|
-
nestedScrollEnabled
|
|
168
|
-
>
|
|
169
|
-
{filtered.map((option) => {
|
|
170
|
-
const isSelected = option.value === value;
|
|
171
|
-
const isDisabled = option.disabled;
|
|
172
|
-
|
|
173
|
-
return (
|
|
174
|
-
<Pressable
|
|
175
|
-
key={option.value}
|
|
176
|
-
onPress={() => !isDisabled && selectOption(option)}
|
|
177
|
-
disabled={isDisabled}
|
|
178
|
-
accessibilityRole="menuitem"
|
|
179
|
-
accessibilityLabel={option.label}
|
|
180
|
-
accessibilityState={{ selected: isSelected, disabled: isDisabled }}
|
|
181
|
-
style={({ pressed }) => [
|
|
182
|
-
styles.option,
|
|
183
|
-
isSelected && { backgroundColor: colors.colorBlueSubtle },
|
|
184
|
-
pressed && !isDisabled && { backgroundColor: colors.colorBgSubtle },
|
|
185
|
-
isDisabled && { opacity: 0.4 },
|
|
186
|
-
]}
|
|
187
|
-
>
|
|
188
|
-
<Text
|
|
189
|
-
style={[
|
|
190
|
-
styles.optionText,
|
|
191
|
-
{
|
|
192
|
-
color: isSelected
|
|
193
|
-
? colors.colorInteractive
|
|
194
|
-
: colors.colorOnBg,
|
|
195
|
-
},
|
|
196
|
-
isSelected && { fontWeight: font.weight.semibold },
|
|
197
|
-
]}
|
|
198
|
-
>
|
|
199
|
-
{option.label}
|
|
200
|
-
</Text>
|
|
201
|
-
</Pressable>
|
|
202
|
-
);
|
|
203
|
-
})}
|
|
204
|
-
</ScrollView>
|
|
205
|
-
)}
|
|
206
|
-
</View>
|
|
207
|
-
<Message type="error" message={error} />
|
|
208
|
-
<Message type="hint" message={hint} />
|
|
209
|
-
</View>
|
|
210
|
-
);
|
|
211
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
|
|
2
|
-
import * as Stories from './FileInput.native.stories';
|
|
3
|
-
|
|
4
|
-
<Meta of={Stories} />
|
|
5
|
-
|
|
6
|
-
# FileInput (React Native)
|
|
7
|
-
|
|
8
|
-
A file selection area that displays an upload prompt. In React Native, actual file picking requires a native module (e.g. `expo-document-picker`). This component provides the UI trigger and displays selected file names.
|
|
9
|
-
|
|
10
|
-
> **Status: alpha** -- API may change before stable release.
|
|
11
|
-
|
|
12
|
-
## Usage
|
|
13
|
-
|
|
14
|
-
```tsx
|
|
15
|
-
import { FileInput } from '@boostdev/design-system-components';
|
|
16
|
-
import { useState } from 'react';
|
|
17
|
-
import * as DocumentPicker from 'expo-document-picker';
|
|
18
|
-
|
|
19
|
-
function Example() {
|
|
20
|
-
const [fileNames, setFileNames] = useState<string[]>([]);
|
|
21
|
-
|
|
22
|
-
const handleSelect = async () => {
|
|
23
|
-
const result = await DocumentPicker.getDocumentAsync({ multiple: true });
|
|
24
|
-
if (!result.canceled) {
|
|
25
|
-
setFileNames(result.assets.map(a => a.name));
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
return (
|
|
30
|
-
<FileInput
|
|
31
|
-
label="Upload document"
|
|
32
|
-
name="document"
|
|
33
|
-
fileNames={fileNames}
|
|
34
|
-
onSelect={handleSelect}
|
|
35
|
-
/>
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## Examples
|
|
41
|
-
|
|
42
|
-
### Default
|
|
43
|
-
<Canvas of={Stories.Default} />
|
|
44
|
-
|
|
45
|
-
### With accepted file types
|
|
46
|
-
<Canvas of={Stories.WithAccept} />
|
|
47
|
-
|
|
48
|
-
### With selected files
|
|
49
|
-
<Canvas of={Stories.WithFiles} />
|
|
50
|
-
|
|
51
|
-
### Required
|
|
52
|
-
<Canvas of={Stories.Required} />
|
|
53
|
-
|
|
54
|
-
### With hint
|
|
55
|
-
<Canvas of={Stories.WithHint} />
|
|
56
|
-
|
|
57
|
-
### With error
|
|
58
|
-
<Canvas of={Stories.WithError} />
|
|
59
|
-
|
|
60
|
-
### Disabled
|
|
61
|
-
<Canvas of={Stories.Disabled} />
|
|
62
|
-
|
|
63
|
-
## Props
|
|
64
|
-
|
|
65
|
-
<ArgTypes of={Stories} />
|
|
66
|
-
|
|
67
|
-
## Props reference
|
|
68
|
-
|
|
69
|
-
| Prop | Type | Default | Description |
|
|
70
|
-
|------|------|---------|-------------|
|
|
71
|
-
| `label` | string | -- | Visible label text |
|
|
72
|
-
| `name` | string | -- | Field name |
|
|
73
|
-
| `accept` | string | -- | Accepted file types hint (display only) |
|
|
74
|
-
| `multiple` | boolean | `false` | Whether multiple files can be selected |
|
|
75
|
-
| `disabled` | boolean | `false` | Prevents interaction, reduces opacity |
|
|
76
|
-
| `required` | boolean | -- | Appends asterisk to label |
|
|
77
|
-
| `error` | string | -- | Error message displayed below |
|
|
78
|
-
| `hint` | string | -- | Helper text displayed below |
|
|
79
|
-
| `onSelect` | `() => void` | -- | Called when the file area is pressed |
|
|
80
|
-
| `fileNames` | `string[]` | `[]` | File names to display |
|
|
81
|
-
|
|
82
|
-
## Accessibility
|
|
83
|
-
|
|
84
|
-
- Uses `accessibilityRole="button"` for the pressable area
|
|
85
|
-
- `accessibilityLabel` includes the label text and action prompt
|
|
86
|
-
- Disabled state is communicated via `accessibilityState`
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { render, screen, fireEvent } from '@testing-library/react';
|
|
2
|
-
import { ThemeProvider } from '../../../../native/ThemeContext';
|
|
3
|
-
import { FileInput } from './FileInput.native';
|
|
4
|
-
|
|
5
|
-
function renderWithTheme(ui: React.ReactElement) {
|
|
6
|
-
return render(<ThemeProvider>{ui}</ThemeProvider>);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
describe('FileInput (native)', () => {
|
|
10
|
-
it('renders with label', () => {
|
|
11
|
-
renderWithTheme(<FileInput label="Upload document" name="document" />);
|
|
12
|
-
expect(screen.getByText('Upload document')).toBeInTheDocument();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('renders the tap-to-select prompt when no files are selected', () => {
|
|
16
|
-
renderWithTheme(<FileInput label="Upload" name="upload" />);
|
|
17
|
-
expect(screen.getByText('Tap to select file')).toBeInTheDocument();
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('displays file names when provided', () => {
|
|
21
|
-
renderWithTheme(
|
|
22
|
-
<FileInput label="Upload" name="upload" fileNames={['report.pdf', 'data.csv']} />,
|
|
23
|
-
);
|
|
24
|
-
expect(screen.getByText('report.pdf')).toBeInTheDocument();
|
|
25
|
-
expect(screen.getByText('data.csv')).toBeInTheDocument();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('displays accept hint', () => {
|
|
29
|
-
renderWithTheme(
|
|
30
|
-
<FileInput label="Upload" name="upload" accept=".png, .jpg" />,
|
|
31
|
-
);
|
|
32
|
-
expect(screen.getByText('.png, .jpg')).toBeInTheDocument();
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('calls onSelect when clicked', () => {
|
|
36
|
-
const onSelect = vi.fn();
|
|
37
|
-
renderWithTheme(
|
|
38
|
-
<FileInput label="Upload" name="upload" onSelect={onSelect} />,
|
|
39
|
-
);
|
|
40
|
-
fireEvent.click(screen.getByRole('button'));
|
|
41
|
-
expect(onSelect).toHaveBeenCalledOnce();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('does not call onSelect when disabled', () => {
|
|
45
|
-
const onSelect = vi.fn();
|
|
46
|
-
renderWithTheme(
|
|
47
|
-
<FileInput label="Upload" name="upload" onSelect={onSelect} disabled />,
|
|
48
|
-
);
|
|
49
|
-
fireEvent.click(screen.getByRole('button'));
|
|
50
|
-
expect(onSelect).not.toHaveBeenCalled();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('renders error message', () => {
|
|
54
|
-
renderWithTheme(
|
|
55
|
-
<FileInput label="Upload" name="upload" error="File required." />,
|
|
56
|
-
);
|
|
57
|
-
expect(screen.getByRole('alert')).toHaveTextContent('File required.');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('renders hint message', () => {
|
|
61
|
-
renderWithTheme(
|
|
62
|
-
<FileInput label="Upload" name="upload" hint="Max 5MB." />,
|
|
63
|
-
);
|
|
64
|
-
expect(screen.getByText('Max 5MB.')).toBeInTheDocument();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('appends asterisk to label when required', () => {
|
|
68
|
-
renderWithTheme(
|
|
69
|
-
<FileInput label="Resume" name="resume" required />,
|
|
70
|
-
);
|
|
71
|
-
expect(screen.getByText('Resume *')).toBeInTheDocument();
|
|
72
|
-
});
|
|
73
|
-
});
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
-
import { ThemeProvider } from '../../../../native/ThemeContext';
|
|
4
|
-
import { FileInput } from './FileInput.native';
|
|
5
|
-
|
|
6
|
-
const meta = {
|
|
7
|
-
tags: ['!stable', 'alpha'],
|
|
8
|
-
title: 'React Native/Form/FileInput',
|
|
9
|
-
component: FileInput,
|
|
10
|
-
decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
|
|
11
|
-
parameters: { layout: 'padded' },
|
|
12
|
-
} satisfies Meta<typeof FileInput>;
|
|
13
|
-
|
|
14
|
-
export default meta;
|
|
15
|
-
type Story = StoryObj<typeof meta>;
|
|
16
|
-
|
|
17
|
-
export const Default: Story = {
|
|
18
|
-
args: { label: 'Upload document', name: 'document' },
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export const WithAccept: Story = {
|
|
22
|
-
args: { label: 'Upload image', name: 'image', accept: '.png, .jpg, .jpeg' },
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export const WithFiles: Story = {
|
|
26
|
-
args: {
|
|
27
|
-
label: 'Upload document',
|
|
28
|
-
name: 'document',
|
|
29
|
-
fileNames: ['report.pdf', 'summary.docx'],
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export const Required: Story = {
|
|
34
|
-
args: { label: 'Upload resume', name: 'resume', required: true },
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export const Disabled: Story = {
|
|
38
|
-
args: { label: 'Upload document', name: 'document', disabled: true },
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export const WithError: Story = {
|
|
42
|
-
args: {
|
|
43
|
-
label: 'Upload document',
|
|
44
|
-
name: 'document',
|
|
45
|
-
error: 'Please upload a file.',
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export const WithHint: Story = {
|
|
50
|
-
args: {
|
|
51
|
-
label: 'Upload photo',
|
|
52
|
-
name: 'photo',
|
|
53
|
-
hint: 'Max file size: 5MB.',
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export const Interactive: Story = {
|
|
58
|
-
render: () => {
|
|
59
|
-
const [files, setFiles] = useState<string[]>([]);
|
|
60
|
-
return (
|
|
61
|
-
<FileInput
|
|
62
|
-
label="Upload document"
|
|
63
|
-
name="document"
|
|
64
|
-
fileNames={files}
|
|
65
|
-
onSelect={() => setFiles(['example.pdf'])}
|
|
66
|
-
/>
|
|
67
|
-
);
|
|
68
|
-
},
|
|
69
|
-
};
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { StyleSheet, View, Pressable, Text, StyleProp, ViewStyle } from 'react-native';
|
|
2
|
-
import { spacing, font, border, lineHeight } from '../../../../native/tokens';
|
|
3
|
-
import { useTheme } from '../../../../native/ThemeContext';
|
|
4
|
-
import { Label } from '../atoms/Label.native';
|
|
5
|
-
import { Message } from '../atoms/Message.native';
|
|
6
|
-
|
|
7
|
-
interface FileInputProps {
|
|
8
|
-
label: string;
|
|
9
|
-
name: string;
|
|
10
|
-
accept?: string;
|
|
11
|
-
multiple?: boolean;
|
|
12
|
-
disabled?: boolean;
|
|
13
|
-
required?: boolean;
|
|
14
|
-
error?: string;
|
|
15
|
-
hint?: string;
|
|
16
|
-
/** Called when the file area is pressed. Wire up a native file picker (e.g. expo-document-picker). */
|
|
17
|
-
onSelect?: () => void;
|
|
18
|
-
/** File names to display (managed externally after picker resolves). */
|
|
19
|
-
fileNames?: string[];
|
|
20
|
-
style?: StyleProp<ViewStyle>;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const styles = StyleSheet.create({
|
|
24
|
-
container: {
|
|
25
|
-
gap: spacing.xxs,
|
|
26
|
-
},
|
|
27
|
-
dropZone: {
|
|
28
|
-
alignItems: 'center',
|
|
29
|
-
justifyContent: 'center',
|
|
30
|
-
gap: spacing.xs,
|
|
31
|
-
paddingVertical: spacing.xl,
|
|
32
|
-
paddingHorizontal: spacing.m,
|
|
33
|
-
borderWidth: 2,
|
|
34
|
-
borderStyle: 'dashed',
|
|
35
|
-
borderRadius: border.radius.s,
|
|
36
|
-
minHeight: 120,
|
|
37
|
-
},
|
|
38
|
-
icon: {
|
|
39
|
-
fontSize: 28,
|
|
40
|
-
lineHeight: 32,
|
|
41
|
-
},
|
|
42
|
-
prompt: {
|
|
43
|
-
fontFamily: font.family.body,
|
|
44
|
-
fontSize: font.size.body,
|
|
45
|
-
lineHeight: lineHeight.body,
|
|
46
|
-
textAlign: 'center',
|
|
47
|
-
},
|
|
48
|
-
acceptHint: {
|
|
49
|
-
fontFamily: font.family.body,
|
|
50
|
-
fontSize: font.size.bodyS,
|
|
51
|
-
lineHeight: lineHeight.bodyS,
|
|
52
|
-
textAlign: 'center',
|
|
53
|
-
},
|
|
54
|
-
fileList: {
|
|
55
|
-
gap: spacing.xxs,
|
|
56
|
-
alignItems: 'center',
|
|
57
|
-
},
|
|
58
|
-
fileName: {
|
|
59
|
-
fontFamily: font.family.body,
|
|
60
|
-
fontSize: font.size.bodyS,
|
|
61
|
-
lineHeight: lineHeight.bodyS,
|
|
62
|
-
},
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
export function FileInput({
|
|
66
|
-
label,
|
|
67
|
-
name,
|
|
68
|
-
accept,
|
|
69
|
-
disabled = false,
|
|
70
|
-
required,
|
|
71
|
-
error,
|
|
72
|
-
hint,
|
|
73
|
-
onSelect,
|
|
74
|
-
fileNames = [],
|
|
75
|
-
style,
|
|
76
|
-
}: Readonly<FileInputProps>) {
|
|
77
|
-
const { colors } = useTheme();
|
|
78
|
-
|
|
79
|
-
const borderColor = error
|
|
80
|
-
? colors.colorError
|
|
81
|
-
: colors.colorBgSubtle;
|
|
82
|
-
|
|
83
|
-
return (
|
|
84
|
-
<View style={[styles.container, style]} testID={name}>
|
|
85
|
-
<Label
|
|
86
|
-
label={required ? `${label} *` : label}
|
|
87
|
-
/>
|
|
88
|
-
<Pressable
|
|
89
|
-
onPress={() => !disabled && onSelect?.()}
|
|
90
|
-
disabled={disabled}
|
|
91
|
-
accessibilityRole="button"
|
|
92
|
-
accessibilityLabel={`${label}, tap to select file`}
|
|
93
|
-
accessibilityState={{ disabled }}
|
|
94
|
-
style={({ pressed }) => [
|
|
95
|
-
styles.dropZone,
|
|
96
|
-
{
|
|
97
|
-
borderColor,
|
|
98
|
-
backgroundColor: 'transparent',
|
|
99
|
-
},
|
|
100
|
-
pressed && { opacity: 0.7 },
|
|
101
|
-
disabled && { opacity: 0.4 },
|
|
102
|
-
]}
|
|
103
|
-
>
|
|
104
|
-
<Text style={[styles.icon, { color: colors.colorInteractive }]}>
|
|
105
|
-
{'\u2191'}
|
|
106
|
-
</Text>
|
|
107
|
-
<Text style={[styles.prompt, { color: colors.colorOnBgSubtle }]}>
|
|
108
|
-
{fileNames.length > 0 ? 'Selected files:' : 'Tap to select file'}
|
|
109
|
-
</Text>
|
|
110
|
-
{fileNames.length > 0 && (
|
|
111
|
-
<View style={styles.fileList}>
|
|
112
|
-
{fileNames.map((fileName) => (
|
|
113
|
-
<Text key={fileName} style={[styles.fileName, { color: colors.colorOnBg }]}>
|
|
114
|
-
{fileName}
|
|
115
|
-
</Text>
|
|
116
|
-
))}
|
|
117
|
-
</View>
|
|
118
|
-
)}
|
|
119
|
-
{accept && (
|
|
120
|
-
<Text style={[styles.acceptHint, { color: colors.colorOnBgSubtle }]}>
|
|
121
|
-
{accept}
|
|
122
|
-
</Text>
|
|
123
|
-
)}
|
|
124
|
-
</Pressable>
|
|
125
|
-
<Message type="error" message={error} />
|
|
126
|
-
<Message type="hint" message={hint} />
|
|
127
|
-
</View>
|
|
128
|
-
);
|
|
129
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
|
|
2
|
-
import * as Stories from './FormInput.native.stories';
|
|
3
|
-
|
|
4
|
-
<Meta of={Stories} />
|
|
5
|
-
|
|
6
|
-
# FormInput (React Native)
|
|
7
|
-
|
|
8
|
-
Text input field with label, optional hint, and error state. Maps the `type` prop to the appropriate keyboard type and secure text entry mode.
|
|
9
|
-
|
|
10
|
-
> **Status: alpha** — API may change before stable release.
|
|
11
|
-
|
|
12
|
-
## Usage
|
|
13
|
-
|
|
14
|
-
```tsx
|
|
15
|
-
import { FormInput } from '@boostdev/design-system-components';
|
|
16
|
-
import { useState } from 'react';
|
|
17
|
-
|
|
18
|
-
function Example() {
|
|
19
|
-
const [email, setEmail] = useState('');
|
|
20
|
-
return (
|
|
21
|
-
<FormInput
|
|
22
|
-
label="Email"
|
|
23
|
-
name="email"
|
|
24
|
-
type="email"
|
|
25
|
-
placeholder="you@example.com"
|
|
26
|
-
value={email}
|
|
27
|
-
onChange={setEmail}
|
|
28
|
-
/>
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## Examples
|
|
34
|
-
|
|
35
|
-
### Default
|
|
36
|
-
<Canvas of={Stories.Default} />
|
|
37
|
-
|
|
38
|
-
### With placeholder
|
|
39
|
-
<Canvas of={Stories.WithPlaceholder} />
|
|
40
|
-
|
|
41
|
-
### Password
|
|
42
|
-
<Canvas of={Stories.Password} />
|
|
43
|
-
|
|
44
|
-
### With error
|
|
45
|
-
<Canvas of={Stories.WithError} />
|
|
46
|
-
|
|
47
|
-
### With hint
|
|
48
|
-
<Canvas of={Stories.WithHint} />
|
|
49
|
-
|
|
50
|
-
### Disabled
|
|
51
|
-
<Canvas of={Stories.Disabled} />
|
|
52
|
-
|
|
53
|
-
### Interactive
|
|
54
|
-
<Canvas of={Stories.Interactive} />
|
|
55
|
-
|
|
56
|
-
## Props
|
|
57
|
-
|
|
58
|
-
<ArgTypes of={Stories} />
|
|
59
|
-
|
|
60
|
-
## Props reference
|
|
61
|
-
|
|
62
|
-
| Prop | Type | Default | Description |
|
|
63
|
-
|------|------|---------|-------------|
|
|
64
|
-
| `label` | ReactNode | — | Visible label text |
|
|
65
|
-
| `name` | string | — | Field name (used for accessibility) |
|
|
66
|
-
| `type` | `'text' \| 'email' \| 'password' \| 'tel' \| 'url' \| 'number' \| 'search'` | `'text'` | Input type, maps to keyboard type |
|
|
67
|
-
| `value` | string | — | Controlled value |
|
|
68
|
-
| `defaultValue` | string | — | Uncontrolled default value |
|
|
69
|
-
| `placeholder` | string | — | Placeholder text |
|
|
70
|
-
| `error` | string | — | Error message displayed below the input |
|
|
71
|
-
| `hint` | string | — | Hint text displayed below the input |
|
|
72
|
-
| `required` | boolean | `false` | Marks the field as required |
|
|
73
|
-
| `disabled` | boolean | `false` | Prevents interaction, reduces opacity |
|
|
74
|
-
| `onChange` | `(value: string) => void` | — | Called with the new text value |
|
|
75
|
-
|
|
76
|
-
## Accessibility
|
|
77
|
-
|
|
78
|
-
- The label is announced by screen readers via `accessibilityLabel`
|
|
79
|
-
- Hint text is announced via `accessibilityHint`
|
|
80
|
-
- `disabled` communicates the disabled state via `accessibilityState`
|
|
81
|
-
- Password type uses `secureTextEntry` for input masking
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { render, screen } from '@testing-library/react';
|
|
2
|
-
import { ThemeProvider } from '../../../../native/ThemeContext';
|
|
3
|
-
import { FormInput } from './FormInput.native';
|
|
4
|
-
|
|
5
|
-
function renderWithTheme(ui: React.ReactElement) {
|
|
6
|
-
return render(<ThemeProvider>{ui}</ThemeProvider>);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
describe('FormInput (native)', () => {
|
|
10
|
-
it('renders the label', () => {
|
|
11
|
-
renderWithTheme(<FormInput label="Full name" name="fullName" />);
|
|
12
|
-
expect(screen.getByText('Full name')).toBeInTheDocument();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('renders with placeholder text', () => {
|
|
16
|
-
renderWithTheme(
|
|
17
|
-
<FormInput label="Email" name="email" placeholder="you@example.com" />,
|
|
18
|
-
);
|
|
19
|
-
expect(screen.getByPlaceholderText('you@example.com')).toBeInTheDocument();
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('renders with a value', () => {
|
|
23
|
-
renderWithTheme(
|
|
24
|
-
<FormInput label="Name" name="name" value="Jane" />,
|
|
25
|
-
);
|
|
26
|
-
expect(screen.getByDisplayValue('Jane')).toBeInTheDocument();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('shows error message', () => {
|
|
30
|
-
renderWithTheme(
|
|
31
|
-
<FormInput label="Email" name="email" error="Invalid email." />,
|
|
32
|
-
);
|
|
33
|
-
expect(screen.getByRole('alert')).toHaveTextContent('Invalid email.');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('shows hint message', () => {
|
|
37
|
-
renderWithTheme(
|
|
38
|
-
<FormInput label="Username" name="username" hint="At least 3 characters." />,
|
|
39
|
-
);
|
|
40
|
-
expect(screen.getByText('At least 3 characters.')).toBeInTheDocument();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('applies disabled opacity', () => {
|
|
44
|
-
const { container } = renderWithTheme(
|
|
45
|
-
<FormInput label="Name" name="name" disabled />,
|
|
46
|
-
);
|
|
47
|
-
// The TextInput should have opacity 0.4 when disabled
|
|
48
|
-
const inputs = container.querySelectorAll('input, [style*="opacity"]');
|
|
49
|
-
expect(inputs.length).toBeGreaterThan(0);
|
|
50
|
-
});
|
|
51
|
-
});
|