@titan-design/react-ui 0.1.0 → 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/package.json +10 -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
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import {
|
|
5
|
+
Popover,
|
|
6
|
+
PopoverTrigger,
|
|
7
|
+
PopoverContent,
|
|
8
|
+
PopoverCloseButton,
|
|
9
|
+
} from './Popover'
|
|
10
|
+
|
|
11
|
+
describe('Popover', () => {
|
|
12
|
+
it('renders trigger element', () => {
|
|
13
|
+
render(
|
|
14
|
+
<Popover>
|
|
15
|
+
<PopoverTrigger>
|
|
16
|
+
<button>Open</button>
|
|
17
|
+
</PopoverTrigger>
|
|
18
|
+
<PopoverContent>
|
|
19
|
+
<span>Content</span>
|
|
20
|
+
</PopoverContent>
|
|
21
|
+
</Popover>
|
|
22
|
+
)
|
|
23
|
+
expect(screen.getByText('Open')).toBeInTheDocument()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('does not show content by default', () => {
|
|
27
|
+
render(
|
|
28
|
+
<Popover>
|
|
29
|
+
<PopoverTrigger>
|
|
30
|
+
<button>Open</button>
|
|
31
|
+
</PopoverTrigger>
|
|
32
|
+
<PopoverContent>
|
|
33
|
+
<span>Hidden content</span>
|
|
34
|
+
</PopoverContent>
|
|
35
|
+
</Popover>
|
|
36
|
+
)
|
|
37
|
+
expect(screen.queryByText('Hidden content')).not.toBeInTheDocument()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('opens popover when trigger is clicked', () => {
|
|
41
|
+
render(
|
|
42
|
+
<Popover>
|
|
43
|
+
<PopoverTrigger>
|
|
44
|
+
<button>Open</button>
|
|
45
|
+
</PopoverTrigger>
|
|
46
|
+
<PopoverContent>
|
|
47
|
+
<span>Visible content</span>
|
|
48
|
+
</PopoverContent>
|
|
49
|
+
</Popover>
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
fireEvent.click(screen.getByText('Open'))
|
|
53
|
+
expect(screen.getByText('Visible content')).toBeInTheDocument()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('closes popover when trigger is clicked again', () => {
|
|
57
|
+
render(
|
|
58
|
+
<Popover>
|
|
59
|
+
<PopoverTrigger>
|
|
60
|
+
<button>Toggle</button>
|
|
61
|
+
</PopoverTrigger>
|
|
62
|
+
<PopoverContent>
|
|
63
|
+
<span>Content</span>
|
|
64
|
+
</PopoverContent>
|
|
65
|
+
</Popover>
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
fireEvent.click(screen.getByText('Toggle'))
|
|
69
|
+
expect(screen.getByText('Content')).toBeInTheDocument()
|
|
70
|
+
|
|
71
|
+
fireEvent.click(screen.getByText('Toggle'))
|
|
72
|
+
expect(screen.queryByText('Content')).not.toBeInTheDocument()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('controlled state', () => {
|
|
76
|
+
it('shows content when isOpen is true', () => {
|
|
77
|
+
render(
|
|
78
|
+
<Popover isOpen>
|
|
79
|
+
<PopoverTrigger>
|
|
80
|
+
<button>Trigger</button>
|
|
81
|
+
</PopoverTrigger>
|
|
82
|
+
<PopoverContent>
|
|
83
|
+
<span>Open content</span>
|
|
84
|
+
</PopoverContent>
|
|
85
|
+
</Popover>
|
|
86
|
+
)
|
|
87
|
+
expect(screen.getByText('Open content')).toBeInTheDocument()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('hides content when isOpen is false', () => {
|
|
91
|
+
render(
|
|
92
|
+
<Popover isOpen={false}>
|
|
93
|
+
<PopoverTrigger>
|
|
94
|
+
<button>Trigger</button>
|
|
95
|
+
</PopoverTrigger>
|
|
96
|
+
<PopoverContent>
|
|
97
|
+
<span>Closed content</span>
|
|
98
|
+
</PopoverContent>
|
|
99
|
+
</Popover>
|
|
100
|
+
)
|
|
101
|
+
expect(screen.queryByText('Closed content')).not.toBeInTheDocument()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('calls onOpenChange when trigger is clicked', () => {
|
|
105
|
+
const onOpenChange = vi.fn()
|
|
106
|
+
render(
|
|
107
|
+
<Popover onOpenChange={onOpenChange}>
|
|
108
|
+
<PopoverTrigger>
|
|
109
|
+
<button>Trigger</button>
|
|
110
|
+
</PopoverTrigger>
|
|
111
|
+
<PopoverContent>
|
|
112
|
+
<span>Content</span>
|
|
113
|
+
</PopoverContent>
|
|
114
|
+
</Popover>
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
fireEvent.click(screen.getByText('Trigger'))
|
|
118
|
+
expect(onOpenChange).toHaveBeenCalledWith(true)
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('placement', () => {
|
|
123
|
+
it('renders with bottom placement (default)', () => {
|
|
124
|
+
render(
|
|
125
|
+
<Popover isOpen placement="bottom">
|
|
126
|
+
<PopoverTrigger>
|
|
127
|
+
<button>Trigger</button>
|
|
128
|
+
</PopoverTrigger>
|
|
129
|
+
<PopoverContent>
|
|
130
|
+
<span>Bottom</span>
|
|
131
|
+
</PopoverContent>
|
|
132
|
+
</Popover>
|
|
133
|
+
)
|
|
134
|
+
expect(screen.getByText('Bottom')).toBeInTheDocument()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('renders with top placement', () => {
|
|
138
|
+
render(
|
|
139
|
+
<Popover isOpen placement="top">
|
|
140
|
+
<PopoverTrigger>
|
|
141
|
+
<button>Trigger</button>
|
|
142
|
+
</PopoverTrigger>
|
|
143
|
+
<PopoverContent>
|
|
144
|
+
<span>Top</span>
|
|
145
|
+
</PopoverContent>
|
|
146
|
+
</Popover>
|
|
147
|
+
)
|
|
148
|
+
expect(screen.getByText('Top')).toBeInTheDocument()
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('renders with left placement', () => {
|
|
152
|
+
render(
|
|
153
|
+
<Popover isOpen placement="left">
|
|
154
|
+
<PopoverTrigger>
|
|
155
|
+
<button>Trigger</button>
|
|
156
|
+
</PopoverTrigger>
|
|
157
|
+
<PopoverContent>
|
|
158
|
+
<span>Left</span>
|
|
159
|
+
</PopoverContent>
|
|
160
|
+
</Popover>
|
|
161
|
+
)
|
|
162
|
+
expect(screen.getByText('Left')).toBeInTheDocument()
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('renders with right placement', () => {
|
|
166
|
+
render(
|
|
167
|
+
<Popover isOpen placement="right">
|
|
168
|
+
<PopoverTrigger>
|
|
169
|
+
<button>Trigger</button>
|
|
170
|
+
</PopoverTrigger>
|
|
171
|
+
<PopoverContent>
|
|
172
|
+
<span>Right</span>
|
|
173
|
+
</PopoverContent>
|
|
174
|
+
</Popover>
|
|
175
|
+
)
|
|
176
|
+
expect(screen.getByText('Right')).toBeInTheDocument()
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
describe('PopoverCloseButton', () => {
|
|
181
|
+
it('closes popover when clicked', () => {
|
|
182
|
+
render(
|
|
183
|
+
<Popover>
|
|
184
|
+
<PopoverTrigger>
|
|
185
|
+
<button>Open</button>
|
|
186
|
+
</PopoverTrigger>
|
|
187
|
+
<PopoverContent>
|
|
188
|
+
<span>Content</span>
|
|
189
|
+
<PopoverCloseButton>
|
|
190
|
+
<span>Close</span>
|
|
191
|
+
</PopoverCloseButton>
|
|
192
|
+
</PopoverContent>
|
|
193
|
+
</Popover>
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
fireEvent.click(screen.getByText('Open'))
|
|
197
|
+
expect(screen.getByText('Content')).toBeInTheDocument()
|
|
198
|
+
|
|
199
|
+
fireEvent.click(screen.getByLabelText('Close popover'))
|
|
200
|
+
expect(screen.queryByText('Content')).not.toBeInTheDocument()
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('has correct accessibility label', () => {
|
|
204
|
+
render(
|
|
205
|
+
<Popover isOpen>
|
|
206
|
+
<PopoverTrigger>
|
|
207
|
+
<button>Open</button>
|
|
208
|
+
</PopoverTrigger>
|
|
209
|
+
<PopoverContent>
|
|
210
|
+
<PopoverCloseButton>
|
|
211
|
+
<span>X</span>
|
|
212
|
+
</PopoverCloseButton>
|
|
213
|
+
</PopoverContent>
|
|
214
|
+
</Popover>
|
|
215
|
+
)
|
|
216
|
+
expect(screen.getByLabelText('Close popover')).toBeInTheDocument()
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
describe('PopoverTrigger', () => {
|
|
221
|
+
it('communicates expanded state', () => {
|
|
222
|
+
render(
|
|
223
|
+
<Popover>
|
|
224
|
+
<PopoverTrigger>
|
|
225
|
+
<span>Trigger</span>
|
|
226
|
+
</PopoverTrigger>
|
|
227
|
+
<PopoverContent>
|
|
228
|
+
<span>Content</span>
|
|
229
|
+
</PopoverContent>
|
|
230
|
+
</Popover>
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
const trigger = screen.getByRole('button', { name: 'Trigger' })
|
|
234
|
+
// react-native-web does not render aria-expanded for accessibilityState.expanded
|
|
235
|
+
expect(trigger).toBeInTheDocument()
|
|
236
|
+
|
|
237
|
+
fireEvent.click(trigger)
|
|
238
|
+
// Verify popover opens by checking for content
|
|
239
|
+
expect(screen.getByText('Content')).toBeInTheDocument()
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
describe('accessibility', () => {
|
|
244
|
+
it('has no accessibility violations when closed', async () => {
|
|
245
|
+
const { container } = render(
|
|
246
|
+
<Popover>
|
|
247
|
+
<PopoverTrigger>
|
|
248
|
+
<span>Open popover</span>
|
|
249
|
+
</PopoverTrigger>
|
|
250
|
+
<PopoverContent>
|
|
251
|
+
<span>Popover content</span>
|
|
252
|
+
</PopoverContent>
|
|
253
|
+
</Popover>
|
|
254
|
+
)
|
|
255
|
+
const results = await axe(container)
|
|
256
|
+
expect(results).toHaveNoViolations()
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('has no accessibility violations when open', async () => {
|
|
260
|
+
const { container } = render(
|
|
261
|
+
<Popover isOpen>
|
|
262
|
+
<PopoverTrigger>
|
|
263
|
+
<span>Open popover</span>
|
|
264
|
+
</PopoverTrigger>
|
|
265
|
+
<PopoverContent>
|
|
266
|
+
<span>Popover content</span>
|
|
267
|
+
<PopoverCloseButton>
|
|
268
|
+
<span>Close</span>
|
|
269
|
+
</PopoverCloseButton>
|
|
270
|
+
</PopoverContent>
|
|
271
|
+
</Popover>
|
|
272
|
+
)
|
|
273
|
+
const results = await axe(container)
|
|
274
|
+
expect(results).toHaveNoViolations()
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
})
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import React, { useState, createContext, useContext } from 'react'
|
|
2
|
+
import { View, Pressable, Modal, type ViewProps } from 'react-native'
|
|
3
|
+
import { cn } from '../../../utils/cn'
|
|
4
|
+
|
|
5
|
+
export type PopoverPlacement = 'top' | 'bottom' | 'left' | 'right'
|
|
6
|
+
|
|
7
|
+
interface PopoverContextType {
|
|
8
|
+
isOpen: boolean
|
|
9
|
+
setIsOpen: (open: boolean) => void
|
|
10
|
+
placement: PopoverPlacement
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const PopoverContext = createContext<PopoverContextType>({
|
|
14
|
+
isOpen: false,
|
|
15
|
+
setIsOpen: () => {},
|
|
16
|
+
placement: 'bottom',
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export interface PopoverProps extends ViewProps {
|
|
20
|
+
/** Placement of the popover */
|
|
21
|
+
placement?: PopoverPlacement
|
|
22
|
+
/** Controlled open state */
|
|
23
|
+
isOpen?: boolean
|
|
24
|
+
/** Callback when open state changes */
|
|
25
|
+
onOpenChange?: (isOpen: boolean) => void
|
|
26
|
+
/** Whether clicking outside closes the popover */
|
|
27
|
+
closeOnClickOutside?: boolean
|
|
28
|
+
/** Additional className */
|
|
29
|
+
className?: string
|
|
30
|
+
children?: React.ReactNode
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Popover component for displaying floating content.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* <Popover>
|
|
38
|
+
* <PopoverTrigger>
|
|
39
|
+
* <Button><ButtonText>Open</ButtonText></Button>
|
|
40
|
+
* </PopoverTrigger>
|
|
41
|
+
* <PopoverContent>
|
|
42
|
+
* <Text>Popover content</Text>
|
|
43
|
+
* </PopoverContent>
|
|
44
|
+
* </Popover>
|
|
45
|
+
*/
|
|
46
|
+
export function Popover({
|
|
47
|
+
placement = 'bottom',
|
|
48
|
+
isOpen: controlledIsOpen,
|
|
49
|
+
onOpenChange,
|
|
50
|
+
closeOnClickOutside = true,
|
|
51
|
+
className,
|
|
52
|
+
children,
|
|
53
|
+
...props
|
|
54
|
+
}: PopoverProps) {
|
|
55
|
+
const [internalIsOpen, setInternalIsOpen] = useState(false)
|
|
56
|
+
const isOpen = controlledIsOpen ?? internalIsOpen
|
|
57
|
+
|
|
58
|
+
const setIsOpen = (open: boolean) => {
|
|
59
|
+
if (controlledIsOpen === undefined) {
|
|
60
|
+
setInternalIsOpen(open)
|
|
61
|
+
}
|
|
62
|
+
onOpenChange?.(open)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<PopoverContext.Provider value={{ isOpen, setIsOpen, placement }}>
|
|
67
|
+
<View className={cn('relative', className)} {...props}>
|
|
68
|
+
{children}
|
|
69
|
+
</View>
|
|
70
|
+
</PopoverContext.Provider>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface PopoverTriggerProps {
|
|
75
|
+
children: React.ReactNode
|
|
76
|
+
className?: string
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Trigger element for the popover.
|
|
81
|
+
*/
|
|
82
|
+
export function PopoverTrigger({ children, className }: PopoverTriggerProps) {
|
|
83
|
+
const { isOpen, setIsOpen } = useContext(PopoverContext)
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<Pressable
|
|
87
|
+
onPress={() => setIsOpen(!isOpen)}
|
|
88
|
+
accessibilityRole="button"
|
|
89
|
+
accessibilityState={{ expanded: isOpen }}
|
|
90
|
+
className={className}
|
|
91
|
+
>
|
|
92
|
+
{children}
|
|
93
|
+
</Pressable>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface PopoverContentProps {
|
|
98
|
+
children?: React.ReactNode
|
|
99
|
+
className?: string
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Content container for the popover.
|
|
104
|
+
*/
|
|
105
|
+
export function PopoverContent({ children, className }: PopoverContentProps) {
|
|
106
|
+
const { isOpen, setIsOpen, placement } = useContext(PopoverContext)
|
|
107
|
+
|
|
108
|
+
if (!isOpen) return null
|
|
109
|
+
|
|
110
|
+
const placementStyles = {
|
|
111
|
+
top: 'bottom-full left-0 mb-2',
|
|
112
|
+
bottom: 'top-full left-0 mt-2',
|
|
113
|
+
left: 'right-full top-0 mr-2',
|
|
114
|
+
right: 'left-full top-0 ml-2',
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<>
|
|
119
|
+
{/* Backdrop */}
|
|
120
|
+
<Pressable
|
|
121
|
+
onPress={() => setIsOpen(false)}
|
|
122
|
+
className="fixed inset-0 z-40"
|
|
123
|
+
style={{ position: 'absolute' }}
|
|
124
|
+
/>
|
|
125
|
+
{/* Content */}
|
|
126
|
+
<View
|
|
127
|
+
className={cn(
|
|
128
|
+
'absolute z-50 min-w-[200px]',
|
|
129
|
+
'bg-surface-elevated rounded-lg shadow-lg border border-border-default',
|
|
130
|
+
'p-4',
|
|
131
|
+
placementStyles[placement],
|
|
132
|
+
className
|
|
133
|
+
)}
|
|
134
|
+
>
|
|
135
|
+
{children}
|
|
136
|
+
</View>
|
|
137
|
+
</>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface PopoverCloseButtonProps {
|
|
142
|
+
children?: React.ReactNode
|
|
143
|
+
className?: string
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Button to close the popover.
|
|
148
|
+
*/
|
|
149
|
+
export function PopoverCloseButton({ children, className }: PopoverCloseButtonProps) {
|
|
150
|
+
const { setIsOpen } = useContext(PopoverContext)
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<Pressable
|
|
154
|
+
onPress={() => setIsOpen(false)}
|
|
155
|
+
accessibilityRole="button"
|
|
156
|
+
accessibilityLabel="Close popover"
|
|
157
|
+
className={cn('p-1', className)}
|
|
158
|
+
>
|
|
159
|
+
{children}
|
|
160
|
+
</Pressable>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View, Text } from 'react-native'
|
|
3
|
+
import { Progress, CircularProgress, ProgressSteps } from './Progress'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Progress> = {
|
|
6
|
+
title: 'Components/Progress',
|
|
7
|
+
component: Progress,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
value: {
|
|
11
|
+
control: { type: 'range', min: 0, max: 100, step: 1 },
|
|
12
|
+
},
|
|
13
|
+
size: {
|
|
14
|
+
control: 'select',
|
|
15
|
+
options: ['sm', 'md', 'lg'],
|
|
16
|
+
},
|
|
17
|
+
color: {
|
|
18
|
+
control: 'select',
|
|
19
|
+
options: ['primary', 'secondary', 'success', 'error', 'warning', 'info'],
|
|
20
|
+
},
|
|
21
|
+
isIndeterminate: {
|
|
22
|
+
control: 'boolean',
|
|
23
|
+
},
|
|
24
|
+
showValue: {
|
|
25
|
+
control: 'boolean',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default meta
|
|
31
|
+
type Story = StoryObj<typeof Progress>
|
|
32
|
+
|
|
33
|
+
export const Default: Story = {
|
|
34
|
+
args: {
|
|
35
|
+
value: 60,
|
|
36
|
+
size: 'md',
|
|
37
|
+
color: 'primary',
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const WithLabel: Story = {
|
|
42
|
+
args: {
|
|
43
|
+
value: 45,
|
|
44
|
+
label: 'Uploading files...',
|
|
45
|
+
showValue: true,
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const AllSizes: Story = {
|
|
50
|
+
render: () => (
|
|
51
|
+
<View className="gap-6 w-64">
|
|
52
|
+
<View>
|
|
53
|
+
<Text className="text-text-secondary text-sm mb-2">Small</Text>
|
|
54
|
+
<Progress value={60} size="sm" />
|
|
55
|
+
</View>
|
|
56
|
+
<View>
|
|
57
|
+
<Text className="text-text-secondary text-sm mb-2">Medium</Text>
|
|
58
|
+
<Progress value={60} size="md" />
|
|
59
|
+
</View>
|
|
60
|
+
<View>
|
|
61
|
+
<Text className="text-text-secondary text-sm mb-2">Large</Text>
|
|
62
|
+
<Progress value={60} size="lg" />
|
|
63
|
+
</View>
|
|
64
|
+
</View>
|
|
65
|
+
),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const AllColors: Story = {
|
|
69
|
+
render: () => (
|
|
70
|
+
<View className="gap-4 w-64">
|
|
71
|
+
<Progress value={75} color="primary" label="Primary" showValue />
|
|
72
|
+
<Progress value={75} color="secondary" label="Secondary" showValue />
|
|
73
|
+
<Progress value={75} color="success" label="Success" showValue />
|
|
74
|
+
<Progress value={75} color="error" label="Error" showValue />
|
|
75
|
+
<Progress value={75} color="warning" label="Warning" showValue />
|
|
76
|
+
<Progress value={75} color="info" label="Info" showValue />
|
|
77
|
+
</View>
|
|
78
|
+
),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const Indeterminate: Story = {
|
|
82
|
+
args: {
|
|
83
|
+
isIndeterminate: true,
|
|
84
|
+
label: 'Loading...',
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const CustomFormat: Story = {
|
|
89
|
+
args: {
|
|
90
|
+
value: 750,
|
|
91
|
+
max: 1000,
|
|
92
|
+
showValue: true,
|
|
93
|
+
label: 'Storage used',
|
|
94
|
+
formatValue: (value, max) => `${value} MB / ${max} MB`,
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const Circular: Story = {
|
|
99
|
+
render: () => (
|
|
100
|
+
<View className="flex-row gap-6 items-center">
|
|
101
|
+
<CircularProgress value={25} showValue />
|
|
102
|
+
<CircularProgress value={50} showValue />
|
|
103
|
+
<CircularProgress value={75} showValue />
|
|
104
|
+
<CircularProgress value={100} showValue />
|
|
105
|
+
</View>
|
|
106
|
+
),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const CircularSizes: Story = {
|
|
110
|
+
render: () => (
|
|
111
|
+
<View className="flex-row gap-6 items-center">
|
|
112
|
+
<CircularProgress value={60} size={32} strokeWidth={3} showValue />
|
|
113
|
+
<CircularProgress value={60} size={48} strokeWidth={4} showValue />
|
|
114
|
+
<CircularProgress value={60} size={64} strokeWidth={5} showValue />
|
|
115
|
+
<CircularProgress value={60} size={80} strokeWidth={6} showValue />
|
|
116
|
+
</View>
|
|
117
|
+
),
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const Steps: Story = {
|
|
121
|
+
render: () => (
|
|
122
|
+
<View className="gap-8">
|
|
123
|
+
<ProgressSteps
|
|
124
|
+
currentStep={1}
|
|
125
|
+
totalSteps={4}
|
|
126
|
+
labels={['Cart', 'Shipping', 'Payment', 'Done']}
|
|
127
|
+
/>
|
|
128
|
+
<ProgressSteps
|
|
129
|
+
currentStep={2}
|
|
130
|
+
totalSteps={4}
|
|
131
|
+
color="success"
|
|
132
|
+
labels={['Step 1', 'Step 2', 'Step 3', 'Step 4']}
|
|
133
|
+
/>
|
|
134
|
+
<ProgressSteps
|
|
135
|
+
currentStep={0}
|
|
136
|
+
totalSteps={5}
|
|
137
|
+
size="lg"
|
|
138
|
+
/>
|
|
139
|
+
</View>
|
|
140
|
+
),
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const UsageExamples: Story = {
|
|
144
|
+
render: () => (
|
|
145
|
+
<View className="gap-6">
|
|
146
|
+
{/* File upload */}
|
|
147
|
+
<View className="p-4 bg-surface-elevated rounded-lg">
|
|
148
|
+
<Text className="text-text-primary font-medium mb-3">File Upload</Text>
|
|
149
|
+
<Progress value={67} label="document.pdf" showValue />
|
|
150
|
+
</View>
|
|
151
|
+
|
|
152
|
+
{/* Storage quota */}
|
|
153
|
+
<View className="p-4 bg-surface-elevated rounded-lg">
|
|
154
|
+
<Text className="text-text-primary font-medium mb-3">Storage</Text>
|
|
155
|
+
<Progress
|
|
156
|
+
value={8.5}
|
|
157
|
+
max={10}
|
|
158
|
+
color="warning"
|
|
159
|
+
label="8.5 GB of 10 GB used"
|
|
160
|
+
/>
|
|
161
|
+
</View>
|
|
162
|
+
|
|
163
|
+
{/* Checkout steps */}
|
|
164
|
+
<View className="p-4 bg-surface-elevated rounded-lg">
|
|
165
|
+
<Text className="text-text-primary font-medium mb-3">Checkout Progress</Text>
|
|
166
|
+
<ProgressSteps
|
|
167
|
+
currentStep={2}
|
|
168
|
+
totalSteps={4}
|
|
169
|
+
labels={['Cart', 'Shipping', 'Payment', 'Confirm']}
|
|
170
|
+
/>
|
|
171
|
+
</View>
|
|
172
|
+
|
|
173
|
+
{/* Loading stats */}
|
|
174
|
+
<View className="p-4 bg-surface-elevated rounded-lg flex-row gap-4">
|
|
175
|
+
<View className="items-center">
|
|
176
|
+
<CircularProgress value={87} size={56} showValue color="success" />
|
|
177
|
+
<Text className="text-xs text-text-secondary mt-1">Success Rate</Text>
|
|
178
|
+
</View>
|
|
179
|
+
<View className="items-center">
|
|
180
|
+
<CircularProgress value={45} size={56} showValue color="warning" />
|
|
181
|
+
<Text className="text-xs text-text-secondary mt-1">Performance</Text>
|
|
182
|
+
</View>
|
|
183
|
+
<View className="items-center">
|
|
184
|
+
<CircularProgress value={12} size={56} showValue color="error" />
|
|
185
|
+
<Text className="text-xs text-text-secondary mt-1">Error Rate</Text>
|
|
186
|
+
</View>
|
|
187
|
+
</View>
|
|
188
|
+
</View>
|
|
189
|
+
),
|
|
190
|
+
}
|