@titan-design/react-ui 0.0.2 → 0.1.1
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/dist/{chunk-NA3EES23.mjs → chunk-MGW37MPO.mjs} +26 -4
- package/dist/chunk-MGW37MPO.mjs.map +1 -0
- package/dist/index.d.mts +115 -2
- package/dist/index.d.ts +115 -2
- package/dist/index.js +262 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +225 -3
- package/dist/index.mjs.map +1 -1
- package/dist/theme/index.d.mts +124 -109
- package/dist/theme/index.d.ts +124 -109
- package/dist/theme/index.js +24 -1
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +1 -1
- package/package.json +12 -5
- package/src/components/custom/DateTime/DateTime.stories.tsx +180 -0
- package/src/components/custom/DateTime/DateTime.test.tsx +152 -0
- package/src/components/custom/DateTime/DateTime.tsx +235 -0
- package/src/components/custom/DateTime/index.ts +2 -0
- package/src/components/custom/EmptyState/EmptyState.stories.tsx +99 -0
- package/src/components/custom/EmptyState/EmptyState.test.tsx +106 -0
- package/src/components/custom/EmptyState/EmptyState.tsx +68 -0
- package/src/components/custom/EmptyState/index.ts +2 -0
- package/src/components/custom/Metric/Metric.stories.tsx +133 -0
- package/src/components/custom/Metric/Metric.test.tsx +120 -0
- package/src/components/custom/Metric/Metric.tsx +92 -0
- package/src/components/custom/Metric/index.ts +2 -0
- package/src/components/custom/Sidebar/Sidebar.stories.tsx +227 -0
- package/src/components/custom/Sidebar/Sidebar.test.tsx +185 -0
- package/src/components/custom/Sidebar/Sidebar.tsx +257 -0
- package/src/components/custom/Sidebar/index.ts +18 -0
- package/src/components/custom/Table/Table.stories.tsx +481 -0
- package/src/components/custom/Table/Table.test.tsx +531 -0
- package/src/components/custom/Table/Table.tsx +696 -0
- package/src/components/custom/Table/index.ts +22 -0
- package/src/components/custom/Typography/Typography.stories.tsx +153 -0
- package/src/components/custom/Typography/Typography.test.tsx +107 -0
- package/src/components/custom/Typography/Typography.tsx +218 -0
- package/src/components/custom/Typography/index.ts +19 -0
- package/src/components/custom/index.ts +8 -0
- package/src/components/custom/stepper/Stepper.stories.tsx +245 -0
- package/src/components/custom/stepper/Stepper.test.tsx +230 -0
- package/src/components/custom/stepper/Stepper.tsx +250 -0
- package/src/components/custom/stepper/index.ts +10 -0
- package/src/components/index.ts +5 -0
- package/src/components/ui/alert/Alert.stories.tsx +154 -0
- package/src/components/ui/alert/Alert.test.tsx +202 -0
- package/src/components/ui/alert/Alert.tsx +170 -0
- package/src/components/ui/alert/index.ts +8 -0
- package/src/components/ui/autocomplete/Autocomplete.stories.tsx +299 -0
- package/src/components/ui/autocomplete/Autocomplete.test.tsx +247 -0
- package/src/components/ui/autocomplete/Autocomplete.tsx +332 -0
- package/src/components/ui/autocomplete/index.ts +2 -0
- package/src/components/ui/avatar/Avatar.stories.tsx +105 -0
- package/src/components/ui/avatar/Avatar.test.tsx +135 -0
- package/src/components/ui/avatar/Avatar.tsx +168 -0
- package/src/components/ui/avatar/index.ts +2 -0
- package/src/components/ui/badge/Badge.stories.tsx +100 -0
- package/src/components/ui/badge/Badge.test.tsx +86 -0
- package/src/components/ui/badge/Badge.tsx +105 -0
- package/src/components/ui/badge/index.ts +2 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +70 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.test.tsx +156 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +127 -0
- package/src/components/ui/breadcrumbs/index.ts +2 -0
- package/src/components/ui/button/Button.stories.tsx +216 -0
- package/src/components/ui/button/Button.test.tsx +132 -0
- package/src/components/ui/button/Button.tsx +242 -0
- package/src/components/ui/button/index.ts +10 -0
- package/src/components/ui/card/Card.stories.tsx +443 -0
- package/src/components/ui/card/Card.test.tsx +265 -0
- package/src/components/ui/card/Card.tsx +380 -0
- package/src/components/ui/card/index.ts +20 -0
- package/src/components/ui/checkbox/Checkbox.stories.tsx +126 -0
- package/src/components/ui/checkbox/Checkbox.test.tsx +152 -0
- package/src/components/ui/checkbox/Checkbox.tsx +163 -0
- package/src/components/ui/checkbox/index.ts +2 -0
- package/src/components/ui/chip/Chip.stories.tsx +112 -0
- package/src/components/ui/chip/Chip.test.tsx +119 -0
- package/src/components/ui/chip/Chip.tsx +151 -0
- package/src/components/ui/chip/index.ts +2 -0
- package/src/components/ui/collapse/Collapse.stories.tsx +313 -0
- package/src/components/ui/collapse/Collapse.test.tsx +298 -0
- package/src/components/ui/collapse/Collapse.tsx +275 -0
- package/src/components/ui/collapse/index.ts +18 -0
- package/src/components/ui/data-row/DataRow.stories.tsx +78 -0
- package/src/components/ui/data-row/DataRow.test.tsx +78 -0
- package/src/components/ui/data-row/DataRow.tsx +29 -0
- package/src/components/ui/data-row/index.ts +2 -0
- package/src/components/ui/divider/Divider.stories.tsx +71 -0
- package/src/components/ui/divider/Divider.test.tsx +55 -0
- package/src/components/ui/divider/Divider.tsx +37 -0
- package/src/components/ui/divider/index.ts +2 -0
- package/src/components/ui/drawer/Drawer.stories.tsx +228 -0
- package/src/components/ui/drawer/Drawer.test.tsx +142 -0
- package/src/components/ui/drawer/Drawer.tsx +228 -0
- package/src/components/ui/drawer/index.ts +9 -0
- package/src/components/ui/form-field/FormField.stories.tsx +260 -0
- package/src/components/ui/form-field/FormField.test.tsx +280 -0
- package/src/components/ui/form-field/FormField.tsx +308 -0
- package/src/components/ui/form-field/index.ts +2 -0
- package/src/components/ui/help-tip/HelpTip.stories.tsx +250 -0
- package/src/components/ui/help-tip/HelpTip.test.tsx +198 -0
- package/src/components/ui/help-tip/HelpTip.tsx +229 -0
- package/src/components/ui/help-tip/index.ts +2 -0
- package/src/components/ui/icon-box/IconBox.stories.tsx +139 -0
- package/src/components/ui/icon-box/IconBox.test.tsx +120 -0
- package/src/components/ui/icon-box/IconBox.tsx +57 -0
- package/src/components/ui/icon-box/index.ts +1 -0
- package/src/components/ui/index.ts +36 -0
- package/src/components/ui/input/Input.stories.tsx +192 -0
- package/src/components/ui/input/Input.test.tsx +86 -0
- package/src/components/ui/input/Input.tsx +273 -0
- package/src/components/ui/input/index.ts +2 -0
- package/src/components/ui/link/Link.stories.tsx +78 -0
- package/src/components/ui/link/Link.test.tsx +94 -0
- package/src/components/ui/link/Link.tsx +86 -0
- package/src/components/ui/link/index.ts +2 -0
- package/src/components/ui/list-item/ListItem.stories.tsx +146 -0
- package/src/components/ui/list-item/ListItem.test.tsx +128 -0
- package/src/components/ui/list-item/ListItem.tsx +89 -0
- package/src/components/ui/list-item/index.ts +14 -0
- package/src/components/ui/menu/Menu.stories.tsx +180 -0
- package/src/components/ui/menu/Menu.test.tsx +353 -0
- package/src/components/ui/menu/Menu.tsx +222 -0
- package/src/components/ui/menu/index.ts +2 -0
- package/src/components/ui/modal/Modal.stories.tsx +350 -0
- package/src/components/ui/modal/Modal.test.tsx +159 -0
- package/src/components/ui/modal/Modal.tsx +254 -0
- package/src/components/ui/modal/index.ts +19 -0
- package/src/components/ui/popover/Popover.stories.tsx +190 -0
- package/src/components/ui/popover/Popover.test.tsx +277 -0
- package/src/components/ui/popover/Popover.tsx +162 -0
- package/src/components/ui/popover/index.ts +2 -0
- package/src/components/ui/progress/Progress.stories.tsx +190 -0
- package/src/components/ui/progress/Progress.test.tsx +181 -0
- package/src/components/ui/progress/Progress.tsx +320 -0
- package/src/components/ui/progress/index.ts +9 -0
- package/src/components/ui/radio/Radio.stories.tsx +199 -0
- package/src/components/ui/radio/Radio.test.tsx +166 -0
- package/src/components/ui/radio/Radio.tsx +200 -0
- package/src/components/ui/radio/index.ts +2 -0
- package/src/components/ui/section/Section.stories.tsx +111 -0
- package/src/components/ui/section/Section.test.tsx +121 -0
- package/src/components/ui/section/Section.tsx +52 -0
- package/src/components/ui/section/index.ts +6 -0
- package/src/components/ui/select/Select.stories.tsx +286 -0
- package/src/components/ui/select/Select.test.tsx +180 -0
- package/src/components/ui/select/Select.tsx +236 -0
- package/src/components/ui/select/index.ts +2 -0
- package/src/components/ui/skeleton/Skeleton.stories.tsx +153 -0
- package/src/components/ui/skeleton/Skeleton.test.tsx +162 -0
- package/src/components/ui/skeleton/Skeleton.tsx +229 -0
- package/src/components/ui/skeleton/index.ts +16 -0
- package/src/components/ui/spinner/Spinner.stories.tsx +67 -0
- package/src/components/ui/spinner/Spinner.test.tsx +67 -0
- package/src/components/ui/spinner/Spinner.tsx +72 -0
- package/src/components/ui/spinner/index.ts +2 -0
- package/src/components/ui/stack/Stack.stories.tsx +127 -0
- package/src/components/ui/stack/Stack.test.tsx +184 -0
- package/src/components/ui/stack/Stack.tsx +76 -0
- package/src/components/ui/stack/index.ts +2 -0
- package/src/components/ui/surface/Surface.stories.tsx +91 -0
- package/src/components/ui/surface/Surface.test.tsx +63 -0
- package/src/components/ui/surface/Surface.tsx +51 -0
- package/src/components/ui/surface/index.ts +1 -0
- package/src/components/ui/switch/Switch.stories.tsx +111 -0
- package/src/components/ui/switch/Switch.test.tsx +112 -0
- package/src/components/ui/switch/Switch.tsx +103 -0
- package/src/components/ui/switch/index.ts +2 -0
- package/src/components/ui/tabs/Tabs.stories.tsx +213 -0
- package/src/components/ui/tabs/Tabs.test.tsx +192 -0
- package/src/components/ui/tabs/Tabs.tsx +253 -0
- package/src/components/ui/tabs/index.ts +16 -0
- package/src/components/ui/toast/Toast.stories.tsx +255 -0
- package/src/components/ui/toast/Toast.test.tsx +207 -0
- package/src/components/ui/toast/Toast.tsx +327 -0
- package/src/components/ui/toast/index.ts +8 -0
- package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +415 -0
- package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +224 -0
- package/src/components/ui/toolbar-button/ToolbarButton.tsx +307 -0
- package/src/components/ui/toolbar-button/index.ts +7 -0
- package/src/components/ui/tooltip/Tooltip.stories.tsx +126 -0
- package/src/components/ui/tooltip/Tooltip.test.tsx +179 -0
- package/src/components/ui/tooltip/Tooltip.tsx +143 -0
- package/src/components/ui/tooltip/index.ts +2 -0
- package/src/index.ts +10 -0
- package/src/test/setup.ts +19 -0
- package/src/theme/Colors.stories.tsx +258 -0
- package/src/theme/config.ts +193 -0
- package/src/theme/elevation.stories.tsx +346 -0
- package/src/theme/elevation.test.ts +27 -0
- package/src/theme/elevation.ts +567 -0
- package/src/theme/index.ts +39 -0
- package/src/theme/shadows.stories.tsx +523 -0
- package/src/theme/shadows.ts +439 -0
- package/src/theme/tokens/index.ts +2 -0
- package/src/theme/tokens/primitives.ts +354 -0
- package/src/theme/tokens/semantic.ts +381 -0
- package/src/types/jest-axe.d.ts +16 -0
- package/src/types/nativewind.d.ts +54 -0
- package/src/utils/cn.ts +14 -0
- package/src/utils/colors.ts +140 -0
- package/src/utils/form.ts +188 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/useTheme.ts +72 -0
- package/dist/chunk-NA3EES23.mjs.map +0 -1
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { FormField, FormSection, FormActions, FormRow } from './FormField'
|
|
5
|
+
|
|
6
|
+
describe('FormField', () => {
|
|
7
|
+
it('renders label and children', () => {
|
|
8
|
+
render(
|
|
9
|
+
<FormField label="Email">
|
|
10
|
+
<input type="email" placeholder="Enter email" />
|
|
11
|
+
</FormField>
|
|
12
|
+
)
|
|
13
|
+
expect(screen.getByText('Email')).toBeInTheDocument()
|
|
14
|
+
expect(screen.getByPlaceholderText('Enter email')).toBeInTheDocument()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('renders without a label', () => {
|
|
18
|
+
render(
|
|
19
|
+
<FormField>
|
|
20
|
+
<input placeholder="No label" />
|
|
21
|
+
</FormField>
|
|
22
|
+
)
|
|
23
|
+
expect(screen.getByPlaceholderText('No label')).toBeInTheDocument()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('required indicator', () => {
|
|
27
|
+
it('shows asterisk when isRequired is true', () => {
|
|
28
|
+
render(
|
|
29
|
+
<FormField label="Name" isRequired>
|
|
30
|
+
<input />
|
|
31
|
+
</FormField>
|
|
32
|
+
)
|
|
33
|
+
expect(screen.getByText('*')).toBeInTheDocument()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('does not show asterisk when isRequired is false', () => {
|
|
37
|
+
render(
|
|
38
|
+
<FormField label="Name">
|
|
39
|
+
<input />
|
|
40
|
+
</FormField>
|
|
41
|
+
)
|
|
42
|
+
expect(screen.queryByText('*')).not.toBeInTheDocument()
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('helper and error text', () => {
|
|
47
|
+
it('renders helper text', () => {
|
|
48
|
+
render(
|
|
49
|
+
<FormField label="Email" helperText="We will never share your email.">
|
|
50
|
+
<input />
|
|
51
|
+
</FormField>
|
|
52
|
+
)
|
|
53
|
+
expect(screen.getByText('We will never share your email.')).toBeInTheDocument()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('renders error message', () => {
|
|
57
|
+
render(
|
|
58
|
+
<FormField label="Password" errorMessage="Password is too short.">
|
|
59
|
+
<input />
|
|
60
|
+
</FormField>
|
|
61
|
+
)
|
|
62
|
+
expect(screen.getByText('Password is too short.')).toBeInTheDocument()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('shows error message instead of helper text when both are provided', () => {
|
|
66
|
+
render(
|
|
67
|
+
<FormField
|
|
68
|
+
label="Email"
|
|
69
|
+
helperText="Enter a valid email"
|
|
70
|
+
errorMessage="Invalid email"
|
|
71
|
+
>
|
|
72
|
+
<input />
|
|
73
|
+
</FormField>
|
|
74
|
+
)
|
|
75
|
+
expect(screen.getByText('Invalid email')).toBeInTheDocument()
|
|
76
|
+
expect(screen.queryByText('Enter a valid email')).not.toBeInTheDocument()
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('disabled state', () => {
|
|
81
|
+
it('applies disabled styling when isDisabled is true', () => {
|
|
82
|
+
const { container } = render(
|
|
83
|
+
<FormField label="Name" isDisabled>
|
|
84
|
+
<input />
|
|
85
|
+
</FormField>
|
|
86
|
+
)
|
|
87
|
+
expect(container).toBeInTheDocument()
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('label sizes', () => {
|
|
92
|
+
it('renders with sm label size', () => {
|
|
93
|
+
render(
|
|
94
|
+
<FormField label="Small Label" labelSize="sm">
|
|
95
|
+
<input />
|
|
96
|
+
</FormField>
|
|
97
|
+
)
|
|
98
|
+
expect(screen.getByText('Small Label')).toBeInTheDocument()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('renders with md label size (default)', () => {
|
|
102
|
+
render(
|
|
103
|
+
<FormField label="Medium Label" labelSize="md">
|
|
104
|
+
<input />
|
|
105
|
+
</FormField>
|
|
106
|
+
)
|
|
107
|
+
expect(screen.getByText('Medium Label')).toBeInTheDocument()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('renders with lg label size', () => {
|
|
111
|
+
render(
|
|
112
|
+
<FormField label="Large Label" labelSize="lg">
|
|
113
|
+
<input />
|
|
114
|
+
</FormField>
|
|
115
|
+
)
|
|
116
|
+
expect(screen.getByText('Large Label')).toBeInTheDocument()
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('orientation', () => {
|
|
121
|
+
it('renders in vertical orientation (default)', () => {
|
|
122
|
+
const { container } = render(
|
|
123
|
+
<FormField label="Vertical" orientation="vertical">
|
|
124
|
+
<input />
|
|
125
|
+
</FormField>
|
|
126
|
+
)
|
|
127
|
+
expect(container).toBeInTheDocument()
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('renders in horizontal orientation', () => {
|
|
131
|
+
const { container } = render(
|
|
132
|
+
<FormField label="Horizontal" orientation="horizontal" labelWidth={120}>
|
|
133
|
+
<input />
|
|
134
|
+
</FormField>
|
|
135
|
+
)
|
|
136
|
+
expect(container).toBeInTheDocument()
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe('help content', () => {
|
|
141
|
+
it('renders help indicator when helpContent is provided', () => {
|
|
142
|
+
render(
|
|
143
|
+
<FormField label="API Key" helpContent="Found in settings">
|
|
144
|
+
<input />
|
|
145
|
+
</FormField>
|
|
146
|
+
)
|
|
147
|
+
expect(screen.getByText('API Key')).toBeInTheDocument()
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
describe('accessibility', () => {
|
|
152
|
+
it('has no accessibility violations', async () => {
|
|
153
|
+
const { container } = render(
|
|
154
|
+
<FormField label="Email" isRequired helperText="Enter a valid email">
|
|
155
|
+
<input type="email" aria-label="Email" />
|
|
156
|
+
</FormField>
|
|
157
|
+
)
|
|
158
|
+
const results = await axe(container)
|
|
159
|
+
expect(results).toHaveNoViolations()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('has no accessibility violations with error', async () => {
|
|
163
|
+
const { container } = render(
|
|
164
|
+
<FormField label="Password" errorMessage="Too short">
|
|
165
|
+
<input type="password" aria-label="Password" />
|
|
166
|
+
</FormField>
|
|
167
|
+
)
|
|
168
|
+
const results = await axe(container)
|
|
169
|
+
expect(results).toHaveNoViolations()
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
describe('FormSection', () => {
|
|
175
|
+
it('renders title and description', () => {
|
|
176
|
+
render(
|
|
177
|
+
<FormSection title="Personal Info" description="Enter your details">
|
|
178
|
+
<span>Form fields</span>
|
|
179
|
+
</FormSection>
|
|
180
|
+
)
|
|
181
|
+
expect(screen.getByText('Personal Info')).toBeInTheDocument()
|
|
182
|
+
expect(screen.getByText('Enter your details')).toBeInTheDocument()
|
|
183
|
+
expect(screen.getByText('Form fields')).toBeInTheDocument()
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('renders without title and description', () => {
|
|
187
|
+
render(
|
|
188
|
+
<FormSection>
|
|
189
|
+
<span>Just fields</span>
|
|
190
|
+
</FormSection>
|
|
191
|
+
)
|
|
192
|
+
expect(screen.getByText('Just fields')).toBeInTheDocument()
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('has group accessibility role', () => {
|
|
196
|
+
render(
|
|
197
|
+
<FormSection title="Section">
|
|
198
|
+
<span>Content</span>
|
|
199
|
+
</FormSection>
|
|
200
|
+
)
|
|
201
|
+
expect(screen.getByRole('group')).toBeInTheDocument()
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
describe('accessibility', () => {
|
|
205
|
+
it('has no accessibility violations', async () => {
|
|
206
|
+
const { container } = render(
|
|
207
|
+
<FormSection title="Section" description="Description">
|
|
208
|
+
<span>Content</span>
|
|
209
|
+
</FormSection>
|
|
210
|
+
)
|
|
211
|
+
const results = await axe(container)
|
|
212
|
+
expect(results).toHaveNoViolations()
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
describe('FormActions', () => {
|
|
218
|
+
it('renders children', () => {
|
|
219
|
+
render(
|
|
220
|
+
<FormActions>
|
|
221
|
+
<button>Cancel</button>
|
|
222
|
+
<button>Submit</button>
|
|
223
|
+
</FormActions>
|
|
224
|
+
)
|
|
225
|
+
expect(screen.getByText('Cancel')).toBeInTheDocument()
|
|
226
|
+
expect(screen.getByText('Submit')).toBeInTheDocument()
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('renders with different alignments', () => {
|
|
230
|
+
const { rerender } = render(
|
|
231
|
+
<FormActions align="left">
|
|
232
|
+
<button>Action</button>
|
|
233
|
+
</FormActions>
|
|
234
|
+
)
|
|
235
|
+
expect(screen.getByText('Action')).toBeInTheDocument()
|
|
236
|
+
|
|
237
|
+
rerender(
|
|
238
|
+
<FormActions align="center">
|
|
239
|
+
<button>Action</button>
|
|
240
|
+
</FormActions>
|
|
241
|
+
)
|
|
242
|
+
expect(screen.getByText('Action')).toBeInTheDocument()
|
|
243
|
+
|
|
244
|
+
rerender(
|
|
245
|
+
<FormActions align="between">
|
|
246
|
+
<button>Action</button>
|
|
247
|
+
</FormActions>
|
|
248
|
+
)
|
|
249
|
+
expect(screen.getByText('Action')).toBeInTheDocument()
|
|
250
|
+
})
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
describe('FormRow', () => {
|
|
254
|
+
it('renders children in a row', () => {
|
|
255
|
+
render(
|
|
256
|
+
<FormRow>
|
|
257
|
+
<span>Field 1</span>
|
|
258
|
+
<span>Field 2</span>
|
|
259
|
+
</FormRow>
|
|
260
|
+
)
|
|
261
|
+
expect(screen.getByText('Field 1')).toBeInTheDocument()
|
|
262
|
+
expect(screen.getByText('Field 2')).toBeInTheDocument()
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('renders with different gap sizes', () => {
|
|
266
|
+
const { rerender } = render(
|
|
267
|
+
<FormRow gap="sm">
|
|
268
|
+
<span>Field</span>
|
|
269
|
+
</FormRow>
|
|
270
|
+
)
|
|
271
|
+
expect(screen.getByText('Field')).toBeInTheDocument()
|
|
272
|
+
|
|
273
|
+
rerender(
|
|
274
|
+
<FormRow gap="lg">
|
|
275
|
+
<span>Field</span>
|
|
276
|
+
</FormRow>
|
|
277
|
+
)
|
|
278
|
+
expect(screen.getByText('Field')).toBeInTheDocument()
|
|
279
|
+
})
|
|
280
|
+
})
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View, Text, type ViewProps } from 'react-native'
|
|
3
|
+
import { cn } from '../../../utils/cn'
|
|
4
|
+
|
|
5
|
+
export interface FormFieldProps extends ViewProps {
|
|
6
|
+
/** Label text */
|
|
7
|
+
label?: string
|
|
8
|
+
/** Whether the field is required */
|
|
9
|
+
isRequired?: boolean
|
|
10
|
+
/** Helper text shown below the input */
|
|
11
|
+
helperText?: string
|
|
12
|
+
/** Error message (replaces helper text when present) */
|
|
13
|
+
errorMessage?: string
|
|
14
|
+
/** Whether the field is disabled */
|
|
15
|
+
isDisabled?: boolean
|
|
16
|
+
/** Whether the field is read-only */
|
|
17
|
+
isReadOnly?: boolean
|
|
18
|
+
/** Help tooltip content */
|
|
19
|
+
helpContent?: React.ReactNode
|
|
20
|
+
/** Size of the label */
|
|
21
|
+
labelSize?: 'sm' | 'md' | 'lg'
|
|
22
|
+
/** Direction of the layout */
|
|
23
|
+
orientation?: 'vertical' | 'horizontal'
|
|
24
|
+
/** Label width (for horizontal layout) */
|
|
25
|
+
labelWidth?: number | string
|
|
26
|
+
/** Additional className */
|
|
27
|
+
className?: string
|
|
28
|
+
children?: React.ReactNode
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const labelSizeStyles = {
|
|
32
|
+
sm: 'text-xs',
|
|
33
|
+
md: 'text-sm',
|
|
34
|
+
lg: 'text-base',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* FormField component for wrapping form inputs with label, help text, and error states.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* // Basic usage
|
|
42
|
+
* <FormField label="Email" isRequired>
|
|
43
|
+
* <Input placeholder="Enter your email" />
|
|
44
|
+
* </FormField>
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* // With error
|
|
48
|
+
* <FormField
|
|
49
|
+
* label="Password"
|
|
50
|
+
* isRequired
|
|
51
|
+
* errorMessage="Password must be at least 8 characters"
|
|
52
|
+
* >
|
|
53
|
+
* <Input type="password" />
|
|
54
|
+
* </FormField>
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* // Horizontal layout
|
|
58
|
+
* <FormField label="Name" orientation="horizontal" labelWidth={100}>
|
|
59
|
+
* <Input />
|
|
60
|
+
* </FormField>
|
|
61
|
+
*/
|
|
62
|
+
export function FormField({
|
|
63
|
+
label,
|
|
64
|
+
isRequired = false,
|
|
65
|
+
helperText,
|
|
66
|
+
errorMessage,
|
|
67
|
+
isDisabled = false,
|
|
68
|
+
isReadOnly = false,
|
|
69
|
+
helpContent,
|
|
70
|
+
labelSize = 'sm',
|
|
71
|
+
orientation = 'vertical',
|
|
72
|
+
labelWidth,
|
|
73
|
+
className,
|
|
74
|
+
children,
|
|
75
|
+
...props
|
|
76
|
+
}: FormFieldProps) {
|
|
77
|
+
const isInvalid = !!errorMessage
|
|
78
|
+
const isHorizontal = orientation === 'horizontal'
|
|
79
|
+
|
|
80
|
+
// Generate unique IDs for accessibility
|
|
81
|
+
const fieldId = React.useId()
|
|
82
|
+
const helperId = `${fieldId}-helper`
|
|
83
|
+
const errorId = `${fieldId}-error`
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<View
|
|
87
|
+
className={cn(
|
|
88
|
+
'w-full',
|
|
89
|
+
isHorizontal && 'flex-row items-start',
|
|
90
|
+
isDisabled && 'opacity-50',
|
|
91
|
+
className
|
|
92
|
+
)}
|
|
93
|
+
{...props}
|
|
94
|
+
>
|
|
95
|
+
{/* Label Section */}
|
|
96
|
+
{label && (
|
|
97
|
+
<View
|
|
98
|
+
className={cn(
|
|
99
|
+
'flex-row items-center',
|
|
100
|
+
isHorizontal ? 'pt-2' : 'mb-1.5'
|
|
101
|
+
)}
|
|
102
|
+
style={isHorizontal && labelWidth ? { width: labelWidth as number } : undefined}
|
|
103
|
+
>
|
|
104
|
+
<Text
|
|
105
|
+
className={cn(
|
|
106
|
+
'font-medium text-text-primary',
|
|
107
|
+
labelSizeStyles[labelSize]
|
|
108
|
+
)}
|
|
109
|
+
>
|
|
110
|
+
{label}
|
|
111
|
+
{isRequired && (
|
|
112
|
+
<Text className="text-status-error ml-0.5">*</Text>
|
|
113
|
+
)}
|
|
114
|
+
</Text>
|
|
115
|
+
|
|
116
|
+
{helpContent && (
|
|
117
|
+
<View className="ml-1">
|
|
118
|
+
{/* Inline help indicator - could use HelpTip here */}
|
|
119
|
+
<Text className="text-text-tertiary text-xs">ℹ</Text>
|
|
120
|
+
</View>
|
|
121
|
+
)}
|
|
122
|
+
</View>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
{/* Input Section */}
|
|
126
|
+
<View className={cn(isHorizontal && 'flex-1')}>
|
|
127
|
+
{/* Clone children to pass accessibility props */}
|
|
128
|
+
{React.Children.map(children, (child) => {
|
|
129
|
+
if (React.isValidElement(child)) {
|
|
130
|
+
const childProps = child.props as Record<string, any>
|
|
131
|
+
return React.cloneElement(child as React.ReactElement<any>, {
|
|
132
|
+
accessibilityHint: errorMessage || helperText || childProps.accessibilityHint,
|
|
133
|
+
accessibilityState: {
|
|
134
|
+
...childProps.accessibilityState,
|
|
135
|
+
disabled: isDisabled || childProps.accessibilityState?.disabled,
|
|
136
|
+
...(isInvalid ? { invalid: true } : {}),
|
|
137
|
+
},
|
|
138
|
+
isDisabled: isDisabled || childProps.isDisabled,
|
|
139
|
+
isReadOnly: isReadOnly || childProps.isReadOnly,
|
|
140
|
+
isInvalid: isInvalid || childProps.isInvalid,
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
return child
|
|
144
|
+
})}
|
|
145
|
+
|
|
146
|
+
{/* Helper/Error Text */}
|
|
147
|
+
{(helperText || errorMessage) && (
|
|
148
|
+
<Text
|
|
149
|
+
id={isInvalid ? errorId : helperId}
|
|
150
|
+
className={cn(
|
|
151
|
+
'text-xs mt-1.5',
|
|
152
|
+
isInvalid ? 'text-status-error' : 'text-text-tertiary'
|
|
153
|
+
)}
|
|
154
|
+
>
|
|
155
|
+
{errorMessage || helperText}
|
|
156
|
+
</Text>
|
|
157
|
+
)}
|
|
158
|
+
</View>
|
|
159
|
+
</View>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface FormSectionProps extends ViewProps {
|
|
164
|
+
/** Section title */
|
|
165
|
+
title?: string
|
|
166
|
+
/** Section description */
|
|
167
|
+
description?: string
|
|
168
|
+
/** Additional className */
|
|
169
|
+
className?: string
|
|
170
|
+
children?: React.ReactNode
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* FormSection component for grouping related form fields.
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* <FormSection title="Personal Information" description="Enter your basic details">
|
|
178
|
+
* <FormField label="Name">
|
|
179
|
+
* <Input />
|
|
180
|
+
* </FormField>
|
|
181
|
+
* <FormField label="Email">
|
|
182
|
+
* <Input />
|
|
183
|
+
* </FormField>
|
|
184
|
+
* </FormSection>
|
|
185
|
+
*/
|
|
186
|
+
export function FormSection({
|
|
187
|
+
title,
|
|
188
|
+
description,
|
|
189
|
+
className,
|
|
190
|
+
children,
|
|
191
|
+
...props
|
|
192
|
+
}: FormSectionProps) {
|
|
193
|
+
return (
|
|
194
|
+
<View
|
|
195
|
+
className={cn('w-full', className)}
|
|
196
|
+
accessibilityRole={'group' as any}
|
|
197
|
+
accessibilityLabel={title}
|
|
198
|
+
{...props}
|
|
199
|
+
>
|
|
200
|
+
{(title || description) && (
|
|
201
|
+
<View className="mb-4">
|
|
202
|
+
{title && (
|
|
203
|
+
<Text className="text-lg font-semibold text-text-primary">
|
|
204
|
+
{title}
|
|
205
|
+
</Text>
|
|
206
|
+
)}
|
|
207
|
+
{description && (
|
|
208
|
+
<Text className="text-sm text-text-secondary mt-1">
|
|
209
|
+
{description}
|
|
210
|
+
</Text>
|
|
211
|
+
)}
|
|
212
|
+
</View>
|
|
213
|
+
)}
|
|
214
|
+
|
|
215
|
+
<View className="gap-4">
|
|
216
|
+
{children}
|
|
217
|
+
</View>
|
|
218
|
+
</View>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export interface FormActionsProps extends ViewProps {
|
|
223
|
+
/** Alignment of actions */
|
|
224
|
+
align?: 'left' | 'center' | 'right' | 'between'
|
|
225
|
+
/** Additional className */
|
|
226
|
+
className?: string
|
|
227
|
+
children?: React.ReactNode
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const alignStyles = {
|
|
231
|
+
left: 'justify-start',
|
|
232
|
+
center: 'justify-center',
|
|
233
|
+
right: 'justify-end',
|
|
234
|
+
between: 'justify-between',
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* FormActions component for form action buttons.
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* <FormActions align="right">
|
|
242
|
+
* <Button variant="ghost">Cancel</Button>
|
|
243
|
+
* <Button>Submit</Button>
|
|
244
|
+
* </FormActions>
|
|
245
|
+
*/
|
|
246
|
+
export function FormActions({
|
|
247
|
+
align = 'right',
|
|
248
|
+
className,
|
|
249
|
+
children,
|
|
250
|
+
...props
|
|
251
|
+
}: FormActionsProps) {
|
|
252
|
+
return (
|
|
253
|
+
<View
|
|
254
|
+
className={cn(
|
|
255
|
+
'flex-row items-center gap-3 pt-4',
|
|
256
|
+
'border-t border-border-default mt-4',
|
|
257
|
+
alignStyles[align],
|
|
258
|
+
className
|
|
259
|
+
)}
|
|
260
|
+
{...props}
|
|
261
|
+
>
|
|
262
|
+
{children}
|
|
263
|
+
</View>
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export interface FormRowProps extends ViewProps {
|
|
268
|
+
/** Gap between items */
|
|
269
|
+
gap?: 'sm' | 'md' | 'lg'
|
|
270
|
+
/** Additional className */
|
|
271
|
+
className?: string
|
|
272
|
+
children?: React.ReactNode
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const gapStyles = {
|
|
276
|
+
sm: 'gap-2',
|
|
277
|
+
md: 'gap-4',
|
|
278
|
+
lg: 'gap-6',
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* FormRow component for laying out multiple form fields horizontally.
|
|
283
|
+
*
|
|
284
|
+
* @example
|
|
285
|
+
* <FormRow>
|
|
286
|
+
* <FormField label="First Name" className="flex-1">
|
|
287
|
+
* <Input />
|
|
288
|
+
* </FormField>
|
|
289
|
+
* <FormField label="Last Name" className="flex-1">
|
|
290
|
+
* <Input />
|
|
291
|
+
* </FormField>
|
|
292
|
+
* </FormRow>
|
|
293
|
+
*/
|
|
294
|
+
export function FormRow({
|
|
295
|
+
gap = 'md',
|
|
296
|
+
className,
|
|
297
|
+
children,
|
|
298
|
+
...props
|
|
299
|
+
}: FormRowProps) {
|
|
300
|
+
return (
|
|
301
|
+
<View
|
|
302
|
+
className={cn('flex-row items-start', gapStyles[gap], className)}
|
|
303
|
+
{...props}
|
|
304
|
+
>
|
|
305
|
+
{children}
|
|
306
|
+
</View>
|
|
307
|
+
)
|
|
308
|
+
}
|