@titan-design/react-ui 0.1.0 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -1
- package/dist/{chunk-MGW37MPO.mjs → chunk-UXVRPOME.mjs} +173 -3
- package/dist/chunk-UXVRPOME.mjs.map +1 -0
- package/dist/index.d.mts +101 -12
- package/dist/index.d.ts +101 -12
- package/dist/index.js +1267 -468
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +734 -112
- package/dist/index.mjs.map +1 -1
- package/dist/theme/index.d.mts +86 -1
- package/dist/theme/index.d.ts +86 -1
- package/dist/theme/index.js +166 -0
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +1 -1
- package/package.json +22 -6
- 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/Workout/RestTimer.stories.tsx +71 -0
- package/src/components/custom/Workout/RestTimer.test.tsx +145 -0
- package/src/components/custom/Workout/RestTimer.tsx +172 -0
- package/src/components/custom/Workout/SupersetWrapper.stories.tsx +144 -0
- package/src/components/custom/Workout/SupersetWrapper.test.tsx +130 -0
- package/src/components/custom/Workout/SupersetWrapper.tsx +63 -0
- package/src/components/custom/Workout/index.ts +2 -0
- package/src/components/custom/index.ts +9 -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 +152 -0
- package/src/components/ui/avatar/Avatar.tsx +176 -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 +103 -0
- package/src/components/ui/badge/Badge.tsx +119 -0
- package/src/components/ui/badge/index.ts +3 -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 +297 -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 +294 -0
- package/src/components/ui/card/Card.tsx +401 -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 +38 -0
- package/src/components/ui/indicator/Indicator.stories.tsx +74 -0
- package/src/components/ui/indicator/Indicator.test.tsx +55 -0
- package/src/components/ui/indicator/Indicator.tsx +73 -0
- package/src/components/ui/indicator/index.ts +2 -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/pill/Pill.stories.tsx +71 -0
- package/src/components/ui/pill/Pill.test.tsx +69 -0
- package/src/components/ui/pill/Pill.tsx +104 -0
- package/src/components/ui/pill/index.ts +2 -0
- package/src/components/ui/popover/Popover.stories.tsx +190 -0
- package/src/components/ui/popover/Popover.test.tsx +419 -0
- package/src/components/ui/popover/Popover.tsx +237 -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 +210 -0
- package/src/components/ui/progress/Progress.tsx +339 -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 +200 -0
- package/src/components/ui/select/Select.tsx +243 -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 +9 -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 +262 -0
- package/src/components/ui/tooltip/Tooltip.tsx +234 -0
- package/src/components/ui/tooltip/index.ts +2 -0
- package/src/index.ts +10 -0
- package/src/stories/PresetShowcase.stories.tsx +462 -0
- package/src/stories/ThemePresets.stories.tsx +85 -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/global.css +129 -1
- package/src/theme/index.ts +40 -0
- package/src/theme/presets/apply.ts +97 -0
- package/src/theme/presets/audiobook.ts +93 -0
- package/src/theme/presets/default.ts +6 -0
- package/src/theme/presets/index.ts +4 -0
- package/src/theme/presets/presets.test.ts +51 -0
- package/src/theme/presets/types.ts +76 -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/avatar-color.test.ts +30 -0
- package/src/utils/avatar-color.ts +20 -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/src/web-jsx/jsx-dev-runtime.ts +28 -0
- package/src/web-jsx/jsx-runtime.ts +41 -0
- package/tailwind.config.js +53 -3
- package/dist/chunk-MGW37MPO.mjs.map +0 -1
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View, Text } from 'react-native'
|
|
3
|
+
import { Popover, PopoverTrigger, PopoverContent, PopoverCloseButton } from './Popover'
|
|
4
|
+
import { Button, ButtonText } from '../button/Button'
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Popover> = {
|
|
7
|
+
title: 'Components/Popover',
|
|
8
|
+
component: Popover,
|
|
9
|
+
tags: ['autodocs'],
|
|
10
|
+
argTypes: {
|
|
11
|
+
placement: {
|
|
12
|
+
control: 'select',
|
|
13
|
+
options: ['top', 'bottom', 'left', 'right'],
|
|
14
|
+
description: 'Popover placement relative to trigger',
|
|
15
|
+
},
|
|
16
|
+
isOpen: {
|
|
17
|
+
control: 'boolean',
|
|
18
|
+
description: 'Controlled open state',
|
|
19
|
+
},
|
|
20
|
+
closeOnClickOutside: {
|
|
21
|
+
control: 'boolean',
|
|
22
|
+
description: 'Whether clicking outside closes the popover',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
decorators: [
|
|
26
|
+
(Story) => (
|
|
27
|
+
<View style={{ minHeight: 300, padding: 100, alignItems: 'flex-start' }}>
|
|
28
|
+
<Story />
|
|
29
|
+
</View>
|
|
30
|
+
),
|
|
31
|
+
],
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default meta
|
|
35
|
+
type Story = StoryObj<typeof Popover>
|
|
36
|
+
|
|
37
|
+
export const Default: Story = {
|
|
38
|
+
args: {
|
|
39
|
+
placement: 'bottom',
|
|
40
|
+
},
|
|
41
|
+
render: (args) => (
|
|
42
|
+
<Popover {...args}>
|
|
43
|
+
<PopoverTrigger>
|
|
44
|
+
<Button variant="solid" color="primary">
|
|
45
|
+
<ButtonText>Open Popover</ButtonText>
|
|
46
|
+
</Button>
|
|
47
|
+
</PopoverTrigger>
|
|
48
|
+
<PopoverContent>
|
|
49
|
+
<Text className="text-text-primary text-sm font-semibold">Popover Title</Text>
|
|
50
|
+
<Text className="text-text-secondary text-sm mt-1">
|
|
51
|
+
This is a popover with some basic content.
|
|
52
|
+
</Text>
|
|
53
|
+
</PopoverContent>
|
|
54
|
+
</Popover>
|
|
55
|
+
),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const PlacementTop: Story = {
|
|
59
|
+
render: () => (
|
|
60
|
+
<Popover placement="top">
|
|
61
|
+
<PopoverTrigger>
|
|
62
|
+
<Button variant="outline" color="primary">
|
|
63
|
+
<ButtonText>Top Placement</ButtonText>
|
|
64
|
+
</Button>
|
|
65
|
+
</PopoverTrigger>
|
|
66
|
+
<PopoverContent>
|
|
67
|
+
<Text className="text-text-primary text-sm">This popover appears above the trigger.</Text>
|
|
68
|
+
</PopoverContent>
|
|
69
|
+
</Popover>
|
|
70
|
+
),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const PlacementBottom: Story = {
|
|
74
|
+
render: () => (
|
|
75
|
+
<Popover placement="bottom">
|
|
76
|
+
<PopoverTrigger>
|
|
77
|
+
<Button variant="outline" color="primary">
|
|
78
|
+
<ButtonText>Bottom Placement</ButtonText>
|
|
79
|
+
</Button>
|
|
80
|
+
</PopoverTrigger>
|
|
81
|
+
<PopoverContent>
|
|
82
|
+
<Text className="text-text-primary text-sm">This popover appears below the trigger.</Text>
|
|
83
|
+
</PopoverContent>
|
|
84
|
+
</Popover>
|
|
85
|
+
),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const PlacementLeft: Story = {
|
|
89
|
+
render: () => (
|
|
90
|
+
<Popover placement="left">
|
|
91
|
+
<PopoverTrigger>
|
|
92
|
+
<Button variant="outline" color="primary">
|
|
93
|
+
<ButtonText>Left Placement</ButtonText>
|
|
94
|
+
</Button>
|
|
95
|
+
</PopoverTrigger>
|
|
96
|
+
<PopoverContent>
|
|
97
|
+
<Text className="text-text-primary text-sm">This popover appears to the left.</Text>
|
|
98
|
+
</PopoverContent>
|
|
99
|
+
</Popover>
|
|
100
|
+
),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const PlacementRight: Story = {
|
|
104
|
+
render: () => (
|
|
105
|
+
<Popover placement="right">
|
|
106
|
+
<PopoverTrigger>
|
|
107
|
+
<Button variant="outline" color="primary">
|
|
108
|
+
<ButtonText>Right Placement</ButtonText>
|
|
109
|
+
</Button>
|
|
110
|
+
</PopoverTrigger>
|
|
111
|
+
<PopoverContent>
|
|
112
|
+
<Text className="text-text-primary text-sm">This popover appears to the right.</Text>
|
|
113
|
+
</PopoverContent>
|
|
114
|
+
</Popover>
|
|
115
|
+
),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const WithCloseButton: Story = {
|
|
119
|
+
render: () => (
|
|
120
|
+
<Popover placement="bottom">
|
|
121
|
+
<PopoverTrigger>
|
|
122
|
+
<Button variant="solid" color="primary">
|
|
123
|
+
<ButtonText>With Close</ButtonText>
|
|
124
|
+
</Button>
|
|
125
|
+
</PopoverTrigger>
|
|
126
|
+
<PopoverContent>
|
|
127
|
+
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
128
|
+
<Text className="text-text-primary text-sm font-semibold">Settings</Text>
|
|
129
|
+
<PopoverCloseButton>
|
|
130
|
+
<Text className="text-text-secondary text-lg">✕</Text>
|
|
131
|
+
</PopoverCloseButton>
|
|
132
|
+
</View>
|
|
133
|
+
<Text className="text-text-secondary text-sm mt-2">
|
|
134
|
+
Configure your preferences here.
|
|
135
|
+
</Text>
|
|
136
|
+
</PopoverContent>
|
|
137
|
+
</Popover>
|
|
138
|
+
),
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export const WithFormContent: Story = {
|
|
142
|
+
render: () => (
|
|
143
|
+
<Popover placement="bottom">
|
|
144
|
+
<PopoverTrigger>
|
|
145
|
+
<Button variant="solid" color="primary">
|
|
146
|
+
<ButtonText>Edit Name</ButtonText>
|
|
147
|
+
</Button>
|
|
148
|
+
</PopoverTrigger>
|
|
149
|
+
<PopoverContent>
|
|
150
|
+
<View style={{ gap: 12 }}>
|
|
151
|
+
<Text className="text-text-primary text-sm font-semibold">Edit Display Name</Text>
|
|
152
|
+
<Text className="text-text-secondary text-xs">
|
|
153
|
+
Enter your new display name below.
|
|
154
|
+
</Text>
|
|
155
|
+
<View className="bg-surface-inset border border-border-default rounded-md px-3 py-2">
|
|
156
|
+
<Text className="text-text-secondary text-sm">John Doe</Text>
|
|
157
|
+
</View>
|
|
158
|
+
<View style={{ flexDirection: 'row', gap: 8, justifyContent: 'flex-end' }}>
|
|
159
|
+
<PopoverCloseButton>
|
|
160
|
+
<View className="px-3 py-1.5 rounded-md bg-surface-elevated border border-border-default">
|
|
161
|
+
<Text className="text-text-primary text-sm">Cancel</Text>
|
|
162
|
+
</View>
|
|
163
|
+
</PopoverCloseButton>
|
|
164
|
+
<View className="px-3 py-1.5 rounded-md bg-brand-primary">
|
|
165
|
+
<Text className="text-white text-sm font-medium">Save</Text>
|
|
166
|
+
</View>
|
|
167
|
+
</View>
|
|
168
|
+
</View>
|
|
169
|
+
</PopoverContent>
|
|
170
|
+
</Popover>
|
|
171
|
+
),
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export const Controlled: Story = {
|
|
175
|
+
render: () => (
|
|
176
|
+
<Popover isOpen placement="bottom">
|
|
177
|
+
<PopoverTrigger>
|
|
178
|
+
<Button variant="outline" color="primary">
|
|
179
|
+
<ButtonText>Always Open</ButtonText>
|
|
180
|
+
</Button>
|
|
181
|
+
</PopoverTrigger>
|
|
182
|
+
<PopoverContent>
|
|
183
|
+
<Text className="text-text-primary text-sm font-semibold">Controlled Popover</Text>
|
|
184
|
+
<Text className="text-text-secondary text-sm mt-1">
|
|
185
|
+
This popover is controlled and always visible.
|
|
186
|
+
</Text>
|
|
187
|
+
</PopoverContent>
|
|
188
|
+
</Popover>
|
|
189
|
+
),
|
|
190
|
+
}
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { render, screen, fireEvent, act } 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('hover mode', () => {
|
|
244
|
+
beforeEach(() => {
|
|
245
|
+
vi.useFakeTimers()
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
afterEach(() => {
|
|
249
|
+
vi.useRealTimers()
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('opens popover on mouseEnter of trigger', () => {
|
|
253
|
+
render(
|
|
254
|
+
<Popover triggerMode="hover">
|
|
255
|
+
<PopoverTrigger>
|
|
256
|
+
<button>Hover me</button>
|
|
257
|
+
</PopoverTrigger>
|
|
258
|
+
<PopoverContent>
|
|
259
|
+
<span>Hover content</span>
|
|
260
|
+
</PopoverContent>
|
|
261
|
+
</Popover>
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
expect(screen.queryByText('Hover content')).not.toBeInTheDocument()
|
|
265
|
+
fireEvent.mouseEnter(screen.getByText('Hover me'))
|
|
266
|
+
expect(screen.getByText('Hover content')).toBeInTheDocument()
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('closes popover on mouseLeave after closeDelay', () => {
|
|
270
|
+
render(
|
|
271
|
+
<Popover triggerMode="hover" closeDelay={200}>
|
|
272
|
+
<PopoverTrigger>
|
|
273
|
+
<button>Hover me</button>
|
|
274
|
+
</PopoverTrigger>
|
|
275
|
+
<PopoverContent>
|
|
276
|
+
<span>Hover content</span>
|
|
277
|
+
</PopoverContent>
|
|
278
|
+
</Popover>
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
fireEvent.mouseEnter(screen.getByText('Hover me'))
|
|
282
|
+
expect(screen.getByText('Hover content')).toBeInTheDocument()
|
|
283
|
+
|
|
284
|
+
fireEvent.mouseLeave(screen.getByText('Hover me'))
|
|
285
|
+
// Still visible before delay expires
|
|
286
|
+
expect(screen.getByText('Hover content')).toBeInTheDocument()
|
|
287
|
+
|
|
288
|
+
act(() => {
|
|
289
|
+
vi.advanceTimersByTime(200)
|
|
290
|
+
})
|
|
291
|
+
expect(screen.queryByText('Hover content')).not.toBeInTheDocument()
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('cancels close when hovering back onto trigger', () => {
|
|
295
|
+
render(
|
|
296
|
+
<Popover triggerMode="hover" closeDelay={150}>
|
|
297
|
+
<PopoverTrigger>
|
|
298
|
+
<button>Hover me</button>
|
|
299
|
+
</PopoverTrigger>
|
|
300
|
+
<PopoverContent>
|
|
301
|
+
<span>Hover content</span>
|
|
302
|
+
</PopoverContent>
|
|
303
|
+
</Popover>
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
fireEvent.mouseEnter(screen.getByText('Hover me'))
|
|
307
|
+
expect(screen.getByText('Hover content')).toBeInTheDocument()
|
|
308
|
+
|
|
309
|
+
fireEvent.mouseLeave(screen.getByText('Hover me'))
|
|
310
|
+
act(() => {
|
|
311
|
+
vi.advanceTimersByTime(50)
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
// Re-enter trigger before delay expires
|
|
315
|
+
fireEvent.mouseEnter(screen.getByText('Hover me'))
|
|
316
|
+
act(() => {
|
|
317
|
+
vi.advanceTimersByTime(150)
|
|
318
|
+
})
|
|
319
|
+
expect(screen.getByText('Hover content')).toBeInTheDocument()
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('cancels close when hovering onto content', () => {
|
|
323
|
+
render(
|
|
324
|
+
<Popover triggerMode="hover" closeDelay={150}>
|
|
325
|
+
<PopoverTrigger>
|
|
326
|
+
<button>Hover me</button>
|
|
327
|
+
</PopoverTrigger>
|
|
328
|
+
<PopoverContent>
|
|
329
|
+
<span>Hover content</span>
|
|
330
|
+
</PopoverContent>
|
|
331
|
+
</Popover>
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
fireEvent.mouseEnter(screen.getByText('Hover me'))
|
|
335
|
+
fireEvent.mouseLeave(screen.getByText('Hover me'))
|
|
336
|
+
|
|
337
|
+
// Hover onto the content text (inside PopoverContent)
|
|
338
|
+
fireEvent.mouseEnter(screen.getByText('Hover content'))
|
|
339
|
+
act(() => {
|
|
340
|
+
vi.advanceTimersByTime(300)
|
|
341
|
+
})
|
|
342
|
+
expect(screen.getByText('Hover content')).toBeInTheDocument()
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('still closes on click outside in hover mode', () => {
|
|
346
|
+
render(
|
|
347
|
+
<Popover triggerMode="hover">
|
|
348
|
+
<PopoverTrigger>
|
|
349
|
+
<button>Hover me</button>
|
|
350
|
+
</PopoverTrigger>
|
|
351
|
+
<PopoverContent>
|
|
352
|
+
<span>Hover content</span>
|
|
353
|
+
</PopoverContent>
|
|
354
|
+
</Popover>
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
fireEvent.mouseEnter(screen.getByText('Hover me'))
|
|
358
|
+
expect(screen.getByText('Hover content')).toBeInTheDocument()
|
|
359
|
+
|
|
360
|
+
// The backdrop is the click-outside mechanism
|
|
361
|
+
fireEvent.click(screen.getByText('Hover content').parentElement!.previousSibling as Element)
|
|
362
|
+
expect(screen.queryByText('Hover content')).not.toBeInTheDocument()
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
it('defaults to click mode when triggerMode is not specified', () => {
|
|
366
|
+
render(
|
|
367
|
+
<Popover>
|
|
368
|
+
<PopoverTrigger>
|
|
369
|
+
<button>Click me</button>
|
|
370
|
+
</PopoverTrigger>
|
|
371
|
+
<PopoverContent>
|
|
372
|
+
<span>Click content</span>
|
|
373
|
+
</PopoverContent>
|
|
374
|
+
</Popover>
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
fireEvent.mouseEnter(screen.getByText('Click me'))
|
|
378
|
+
expect(screen.queryByText('Click content')).not.toBeInTheDocument()
|
|
379
|
+
|
|
380
|
+
fireEvent.click(screen.getByText('Click me'))
|
|
381
|
+
expect(screen.getByText('Click content')).toBeInTheDocument()
|
|
382
|
+
})
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
describe('accessibility', () => {
|
|
386
|
+
it('has no accessibility violations when closed', async () => {
|
|
387
|
+
const { container } = render(
|
|
388
|
+
<Popover>
|
|
389
|
+
<PopoverTrigger>
|
|
390
|
+
<span>Open popover</span>
|
|
391
|
+
</PopoverTrigger>
|
|
392
|
+
<PopoverContent>
|
|
393
|
+
<span>Popover content</span>
|
|
394
|
+
</PopoverContent>
|
|
395
|
+
</Popover>
|
|
396
|
+
)
|
|
397
|
+
const results = await axe(container)
|
|
398
|
+
expect(results).toHaveNoViolations()
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
it('has no accessibility violations when open', async () => {
|
|
402
|
+
const { container } = render(
|
|
403
|
+
<Popover isOpen>
|
|
404
|
+
<PopoverTrigger>
|
|
405
|
+
<span>Open popover</span>
|
|
406
|
+
</PopoverTrigger>
|
|
407
|
+
<PopoverContent>
|
|
408
|
+
<span>Popover content</span>
|
|
409
|
+
<PopoverCloseButton>
|
|
410
|
+
<span>Close</span>
|
|
411
|
+
</PopoverCloseButton>
|
|
412
|
+
</PopoverContent>
|
|
413
|
+
</Popover>
|
|
414
|
+
)
|
|
415
|
+
const results = await axe(container)
|
|
416
|
+
expect(results).toHaveNoViolations()
|
|
417
|
+
})
|
|
418
|
+
})
|
|
419
|
+
})
|