@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.
Files changed (236) hide show
  1. package/README.md +47 -1
  2. package/dist/{chunk-MGW37MPO.mjs → chunk-UXVRPOME.mjs} +173 -3
  3. package/dist/chunk-UXVRPOME.mjs.map +1 -0
  4. package/dist/index.d.mts +101 -12
  5. package/dist/index.d.ts +101 -12
  6. package/dist/index.js +1267 -468
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +734 -112
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/theme/index.d.mts +86 -1
  11. package/dist/theme/index.d.ts +86 -1
  12. package/dist/theme/index.js +166 -0
  13. package/dist/theme/index.js.map +1 -1
  14. package/dist/theme/index.mjs +1 -1
  15. package/package.json +22 -6
  16. package/src/components/custom/DateTime/DateTime.stories.tsx +180 -0
  17. package/src/components/custom/DateTime/DateTime.test.tsx +152 -0
  18. package/src/components/custom/DateTime/DateTime.tsx +235 -0
  19. package/src/components/custom/DateTime/index.ts +2 -0
  20. package/src/components/custom/EmptyState/EmptyState.stories.tsx +99 -0
  21. package/src/components/custom/EmptyState/EmptyState.test.tsx +106 -0
  22. package/src/components/custom/EmptyState/EmptyState.tsx +68 -0
  23. package/src/components/custom/EmptyState/index.ts +2 -0
  24. package/src/components/custom/Metric/Metric.stories.tsx +133 -0
  25. package/src/components/custom/Metric/Metric.test.tsx +120 -0
  26. package/src/components/custom/Metric/Metric.tsx +92 -0
  27. package/src/components/custom/Metric/index.ts +2 -0
  28. package/src/components/custom/Sidebar/Sidebar.stories.tsx +227 -0
  29. package/src/components/custom/Sidebar/Sidebar.test.tsx +185 -0
  30. package/src/components/custom/Sidebar/Sidebar.tsx +257 -0
  31. package/src/components/custom/Sidebar/index.ts +18 -0
  32. package/src/components/custom/Table/Table.stories.tsx +481 -0
  33. package/src/components/custom/Table/Table.test.tsx +531 -0
  34. package/src/components/custom/Table/Table.tsx +696 -0
  35. package/src/components/custom/Table/index.ts +22 -0
  36. package/src/components/custom/Typography/Typography.stories.tsx +153 -0
  37. package/src/components/custom/Typography/Typography.test.tsx +107 -0
  38. package/src/components/custom/Typography/Typography.tsx +218 -0
  39. package/src/components/custom/Typography/index.ts +19 -0
  40. package/src/components/custom/Workout/RestTimer.stories.tsx +71 -0
  41. package/src/components/custom/Workout/RestTimer.test.tsx +145 -0
  42. package/src/components/custom/Workout/RestTimer.tsx +172 -0
  43. package/src/components/custom/Workout/SupersetWrapper.stories.tsx +144 -0
  44. package/src/components/custom/Workout/SupersetWrapper.test.tsx +130 -0
  45. package/src/components/custom/Workout/SupersetWrapper.tsx +63 -0
  46. package/src/components/custom/Workout/index.ts +2 -0
  47. package/src/components/custom/index.ts +9 -0
  48. package/src/components/custom/stepper/Stepper.stories.tsx +245 -0
  49. package/src/components/custom/stepper/Stepper.test.tsx +230 -0
  50. package/src/components/custom/stepper/Stepper.tsx +250 -0
  51. package/src/components/custom/stepper/index.ts +10 -0
  52. package/src/components/index.ts +5 -0
  53. package/src/components/ui/alert/Alert.stories.tsx +154 -0
  54. package/src/components/ui/alert/Alert.test.tsx +202 -0
  55. package/src/components/ui/alert/Alert.tsx +170 -0
  56. package/src/components/ui/alert/index.ts +8 -0
  57. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +299 -0
  58. package/src/components/ui/autocomplete/Autocomplete.test.tsx +247 -0
  59. package/src/components/ui/autocomplete/Autocomplete.tsx +332 -0
  60. package/src/components/ui/autocomplete/index.ts +2 -0
  61. package/src/components/ui/avatar/Avatar.stories.tsx +105 -0
  62. package/src/components/ui/avatar/Avatar.test.tsx +152 -0
  63. package/src/components/ui/avatar/Avatar.tsx +176 -0
  64. package/src/components/ui/avatar/index.ts +2 -0
  65. package/src/components/ui/badge/Badge.stories.tsx +100 -0
  66. package/src/components/ui/badge/Badge.test.tsx +103 -0
  67. package/src/components/ui/badge/Badge.tsx +119 -0
  68. package/src/components/ui/badge/index.ts +3 -0
  69. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +70 -0
  70. package/src/components/ui/breadcrumbs/Breadcrumbs.test.tsx +156 -0
  71. package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +127 -0
  72. package/src/components/ui/breadcrumbs/index.ts +2 -0
  73. package/src/components/ui/button/Button.stories.tsx +216 -0
  74. package/src/components/ui/button/Button.test.tsx +132 -0
  75. package/src/components/ui/button/Button.tsx +297 -0
  76. package/src/components/ui/button/index.ts +10 -0
  77. package/src/components/ui/card/Card.stories.tsx +443 -0
  78. package/src/components/ui/card/Card.test.tsx +294 -0
  79. package/src/components/ui/card/Card.tsx +401 -0
  80. package/src/components/ui/card/index.ts +20 -0
  81. package/src/components/ui/checkbox/Checkbox.stories.tsx +126 -0
  82. package/src/components/ui/checkbox/Checkbox.test.tsx +152 -0
  83. package/src/components/ui/checkbox/Checkbox.tsx +163 -0
  84. package/src/components/ui/checkbox/index.ts +2 -0
  85. package/src/components/ui/chip/Chip.stories.tsx +112 -0
  86. package/src/components/ui/chip/Chip.test.tsx +119 -0
  87. package/src/components/ui/chip/Chip.tsx +151 -0
  88. package/src/components/ui/chip/index.ts +2 -0
  89. package/src/components/ui/collapse/Collapse.stories.tsx +313 -0
  90. package/src/components/ui/collapse/Collapse.test.tsx +298 -0
  91. package/src/components/ui/collapse/Collapse.tsx +275 -0
  92. package/src/components/ui/collapse/index.ts +18 -0
  93. package/src/components/ui/data-row/DataRow.stories.tsx +78 -0
  94. package/src/components/ui/data-row/DataRow.test.tsx +78 -0
  95. package/src/components/ui/data-row/DataRow.tsx +29 -0
  96. package/src/components/ui/data-row/index.ts +2 -0
  97. package/src/components/ui/divider/Divider.stories.tsx +71 -0
  98. package/src/components/ui/divider/Divider.test.tsx +55 -0
  99. package/src/components/ui/divider/Divider.tsx +37 -0
  100. package/src/components/ui/divider/index.ts +2 -0
  101. package/src/components/ui/drawer/Drawer.stories.tsx +228 -0
  102. package/src/components/ui/drawer/Drawer.test.tsx +142 -0
  103. package/src/components/ui/drawer/Drawer.tsx +228 -0
  104. package/src/components/ui/drawer/index.ts +9 -0
  105. package/src/components/ui/form-field/FormField.stories.tsx +260 -0
  106. package/src/components/ui/form-field/FormField.test.tsx +280 -0
  107. package/src/components/ui/form-field/FormField.tsx +308 -0
  108. package/src/components/ui/form-field/index.ts +2 -0
  109. package/src/components/ui/help-tip/HelpTip.stories.tsx +250 -0
  110. package/src/components/ui/help-tip/HelpTip.test.tsx +198 -0
  111. package/src/components/ui/help-tip/HelpTip.tsx +229 -0
  112. package/src/components/ui/help-tip/index.ts +2 -0
  113. package/src/components/ui/icon-box/IconBox.stories.tsx +139 -0
  114. package/src/components/ui/icon-box/IconBox.test.tsx +120 -0
  115. package/src/components/ui/icon-box/IconBox.tsx +57 -0
  116. package/src/components/ui/icon-box/index.ts +1 -0
  117. package/src/components/ui/index.ts +38 -0
  118. package/src/components/ui/indicator/Indicator.stories.tsx +74 -0
  119. package/src/components/ui/indicator/Indicator.test.tsx +55 -0
  120. package/src/components/ui/indicator/Indicator.tsx +73 -0
  121. package/src/components/ui/indicator/index.ts +2 -0
  122. package/src/components/ui/input/Input.stories.tsx +192 -0
  123. package/src/components/ui/input/Input.test.tsx +86 -0
  124. package/src/components/ui/input/Input.tsx +273 -0
  125. package/src/components/ui/input/index.ts +2 -0
  126. package/src/components/ui/link/Link.stories.tsx +78 -0
  127. package/src/components/ui/link/Link.test.tsx +94 -0
  128. package/src/components/ui/link/Link.tsx +86 -0
  129. package/src/components/ui/link/index.ts +2 -0
  130. package/src/components/ui/list-item/ListItem.stories.tsx +146 -0
  131. package/src/components/ui/list-item/ListItem.test.tsx +128 -0
  132. package/src/components/ui/list-item/ListItem.tsx +89 -0
  133. package/src/components/ui/list-item/index.ts +14 -0
  134. package/src/components/ui/menu/Menu.stories.tsx +180 -0
  135. package/src/components/ui/menu/Menu.test.tsx +353 -0
  136. package/src/components/ui/menu/Menu.tsx +222 -0
  137. package/src/components/ui/menu/index.ts +2 -0
  138. package/src/components/ui/modal/Modal.stories.tsx +350 -0
  139. package/src/components/ui/modal/Modal.test.tsx +159 -0
  140. package/src/components/ui/modal/Modal.tsx +254 -0
  141. package/src/components/ui/modal/index.ts +19 -0
  142. package/src/components/ui/pill/Pill.stories.tsx +71 -0
  143. package/src/components/ui/pill/Pill.test.tsx +69 -0
  144. package/src/components/ui/pill/Pill.tsx +104 -0
  145. package/src/components/ui/pill/index.ts +2 -0
  146. package/src/components/ui/popover/Popover.stories.tsx +190 -0
  147. package/src/components/ui/popover/Popover.test.tsx +419 -0
  148. package/src/components/ui/popover/Popover.tsx +237 -0
  149. package/src/components/ui/popover/index.ts +2 -0
  150. package/src/components/ui/progress/Progress.stories.tsx +190 -0
  151. package/src/components/ui/progress/Progress.test.tsx +210 -0
  152. package/src/components/ui/progress/Progress.tsx +339 -0
  153. package/src/components/ui/progress/index.ts +9 -0
  154. package/src/components/ui/radio/Radio.stories.tsx +199 -0
  155. package/src/components/ui/radio/Radio.test.tsx +166 -0
  156. package/src/components/ui/radio/Radio.tsx +200 -0
  157. package/src/components/ui/radio/index.ts +2 -0
  158. package/src/components/ui/section/Section.stories.tsx +111 -0
  159. package/src/components/ui/section/Section.test.tsx +121 -0
  160. package/src/components/ui/section/Section.tsx +52 -0
  161. package/src/components/ui/section/index.ts +6 -0
  162. package/src/components/ui/select/Select.stories.tsx +286 -0
  163. package/src/components/ui/select/Select.test.tsx +200 -0
  164. package/src/components/ui/select/Select.tsx +243 -0
  165. package/src/components/ui/select/index.ts +2 -0
  166. package/src/components/ui/skeleton/Skeleton.stories.tsx +153 -0
  167. package/src/components/ui/skeleton/Skeleton.test.tsx +162 -0
  168. package/src/components/ui/skeleton/Skeleton.tsx +229 -0
  169. package/src/components/ui/skeleton/index.ts +16 -0
  170. package/src/components/ui/spinner/Spinner.stories.tsx +67 -0
  171. package/src/components/ui/spinner/Spinner.test.tsx +67 -0
  172. package/src/components/ui/spinner/Spinner.tsx +72 -0
  173. package/src/components/ui/spinner/index.ts +2 -0
  174. package/src/components/ui/stack/Stack.stories.tsx +127 -0
  175. package/src/components/ui/stack/Stack.test.tsx +184 -0
  176. package/src/components/ui/stack/Stack.tsx +76 -0
  177. package/src/components/ui/stack/index.ts +2 -0
  178. package/src/components/ui/surface/Surface.stories.tsx +91 -0
  179. package/src/components/ui/surface/Surface.test.tsx +63 -0
  180. package/src/components/ui/surface/Surface.tsx +51 -0
  181. package/src/components/ui/surface/index.ts +1 -0
  182. package/src/components/ui/switch/Switch.stories.tsx +111 -0
  183. package/src/components/ui/switch/Switch.test.tsx +112 -0
  184. package/src/components/ui/switch/Switch.tsx +103 -0
  185. package/src/components/ui/switch/index.ts +2 -0
  186. package/src/components/ui/tabs/Tabs.stories.tsx +213 -0
  187. package/src/components/ui/tabs/Tabs.test.tsx +192 -0
  188. package/src/components/ui/tabs/Tabs.tsx +253 -0
  189. package/src/components/ui/tabs/index.ts +16 -0
  190. package/src/components/ui/toast/Toast.stories.tsx +255 -0
  191. package/src/components/ui/toast/Toast.test.tsx +207 -0
  192. package/src/components/ui/toast/Toast.tsx +327 -0
  193. package/src/components/ui/toast/index.ts +9 -0
  194. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +415 -0
  195. package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +224 -0
  196. package/src/components/ui/toolbar-button/ToolbarButton.tsx +307 -0
  197. package/src/components/ui/toolbar-button/index.ts +7 -0
  198. package/src/components/ui/tooltip/Tooltip.stories.tsx +126 -0
  199. package/src/components/ui/tooltip/Tooltip.test.tsx +262 -0
  200. package/src/components/ui/tooltip/Tooltip.tsx +234 -0
  201. package/src/components/ui/tooltip/index.ts +2 -0
  202. package/src/index.ts +10 -0
  203. package/src/stories/PresetShowcase.stories.tsx +462 -0
  204. package/src/stories/ThemePresets.stories.tsx +85 -0
  205. package/src/test/setup.ts +19 -0
  206. package/src/theme/Colors.stories.tsx +258 -0
  207. package/src/theme/config.ts +193 -0
  208. package/src/theme/elevation.stories.tsx +346 -0
  209. package/src/theme/elevation.test.ts +27 -0
  210. package/src/theme/elevation.ts +567 -0
  211. package/src/theme/global.css +129 -1
  212. package/src/theme/index.ts +40 -0
  213. package/src/theme/presets/apply.ts +97 -0
  214. package/src/theme/presets/audiobook.ts +93 -0
  215. package/src/theme/presets/default.ts +6 -0
  216. package/src/theme/presets/index.ts +4 -0
  217. package/src/theme/presets/presets.test.ts +51 -0
  218. package/src/theme/presets/types.ts +76 -0
  219. package/src/theme/shadows.stories.tsx +523 -0
  220. package/src/theme/shadows.ts +439 -0
  221. package/src/theme/tokens/index.ts +2 -0
  222. package/src/theme/tokens/primitives.ts +354 -0
  223. package/src/theme/tokens/semantic.ts +381 -0
  224. package/src/types/jest-axe.d.ts +16 -0
  225. package/src/types/nativewind.d.ts +54 -0
  226. package/src/utils/avatar-color.test.ts +30 -0
  227. package/src/utils/avatar-color.ts +20 -0
  228. package/src/utils/cn.ts +14 -0
  229. package/src/utils/colors.ts +140 -0
  230. package/src/utils/form.ts +188 -0
  231. package/src/utils/index.ts +29 -0
  232. package/src/utils/useTheme.ts +72 -0
  233. package/src/web-jsx/jsx-dev-runtime.ts +28 -0
  234. package/src/web-jsx/jsx-runtime.ts +41 -0
  235. package/tailwind.config.js +53 -3
  236. package/dist/chunk-MGW37MPO.mjs.map +0 -1
@@ -0,0 +1,180 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View, Text } from 'react-native'
3
+ import { Menu, MenuTrigger, MenuList, MenuItem, MenuDivider, MenuGroup } from './Menu'
4
+ import { Button, ButtonText } from '../button/Button'
5
+
6
+ const meta: Meta<typeof Menu> = {
7
+ title: 'Components/Menu',
8
+ component: Menu,
9
+ tags: ['autodocs'],
10
+ argTypes: {
11
+ isOpen: {
12
+ control: 'boolean',
13
+ description: 'Controlled open state',
14
+ },
15
+ },
16
+ decorators: [
17
+ (Story) => (
18
+ <View style={{ minHeight: 300, padding: 16 }}>
19
+ <Story />
20
+ </View>
21
+ ),
22
+ ],
23
+ }
24
+
25
+ export default meta
26
+ type Story = StoryObj<typeof Menu>
27
+
28
+ export const Default: Story = {
29
+ render: () => (
30
+ <Menu>
31
+ <MenuTrigger>
32
+ <Button variant="outline" color="primary">
33
+ <ButtonText>Options</ButtonText>
34
+ </Button>
35
+ </MenuTrigger>
36
+ <MenuList>
37
+ <MenuItem onPress={() => {}}>Edit</MenuItem>
38
+ <MenuItem onPress={() => {}}>Duplicate</MenuItem>
39
+ <MenuItem onPress={() => {}}>Share</MenuItem>
40
+ </MenuList>
41
+ </Menu>
42
+ ),
43
+ }
44
+
45
+ export const WithGroups: Story = {
46
+ render: () => (
47
+ <Menu>
48
+ <MenuTrigger>
49
+ <Button variant="outline" color="primary">
50
+ <ButtonText>Actions</ButtonText>
51
+ </Button>
52
+ </MenuTrigger>
53
+ <MenuList>
54
+ <MenuGroup label="Edit">
55
+ <MenuItem onPress={() => {}}>Cut</MenuItem>
56
+ <MenuItem onPress={() => {}}>Copy</MenuItem>
57
+ <MenuItem onPress={() => {}}>Paste</MenuItem>
58
+ </MenuGroup>
59
+ <MenuDivider />
60
+ <MenuGroup label="View">
61
+ <MenuItem onPress={() => {}}>Zoom In</MenuItem>
62
+ <MenuItem onPress={() => {}}>Zoom Out</MenuItem>
63
+ <MenuItem onPress={() => {}}>Reset Zoom</MenuItem>
64
+ </MenuGroup>
65
+ </MenuList>
66
+ </Menu>
67
+ ),
68
+ }
69
+
70
+ function EditIcon({ size = 14 }: { size?: number }) {
71
+ return (
72
+ <Text style={{ fontSize: size, lineHeight: size }}>&#9998;</Text>
73
+ )
74
+ }
75
+
76
+ function TrashIcon({ size = 14 }: { size?: number }) {
77
+ return (
78
+ <Text style={{ fontSize: size, lineHeight: size }}>&#128465;</Text>
79
+ )
80
+ }
81
+
82
+ function CopyIcon({ size = 14 }: { size?: number }) {
83
+ return (
84
+ <Text style={{ fontSize: size, lineHeight: size }}>&#128203;</Text>
85
+ )
86
+ }
87
+
88
+ export const WithIcons: Story = {
89
+ render: () => (
90
+ <Menu>
91
+ <MenuTrigger>
92
+ <Button variant="outline" color="primary">
93
+ <ButtonText>More</ButtonText>
94
+ </Button>
95
+ </MenuTrigger>
96
+ <MenuList>
97
+ <MenuItem icon={<EditIcon />} onPress={() => {}}>Edit</MenuItem>
98
+ <MenuItem icon={<CopyIcon />} onPress={() => {}}>Duplicate</MenuItem>
99
+ <MenuDivider />
100
+ <MenuItem icon={<TrashIcon />} onPress={() => {}} isDestructive>Delete</MenuItem>
101
+ </MenuList>
102
+ </Menu>
103
+ ),
104
+ }
105
+
106
+ export const DestructiveItem: Story = {
107
+ render: () => (
108
+ <Menu>
109
+ <MenuTrigger>
110
+ <Button variant="outline" color="primary">
111
+ <ButtonText>Manage</ButtonText>
112
+ </Button>
113
+ </MenuTrigger>
114
+ <MenuList>
115
+ <MenuItem onPress={() => {}}>Edit Profile</MenuItem>
116
+ <MenuItem onPress={() => {}}>Change Password</MenuItem>
117
+ <MenuDivider />
118
+ <MenuItem onPress={() => {}} isDestructive>Delete Account</MenuItem>
119
+ </MenuList>
120
+ </Menu>
121
+ ),
122
+ }
123
+
124
+ export const WithDividers: Story = {
125
+ render: () => (
126
+ <Menu>
127
+ <MenuTrigger>
128
+ <Button variant="outline" color="primary">
129
+ <ButtonText>File</ButtonText>
130
+ </Button>
131
+ </MenuTrigger>
132
+ <MenuList>
133
+ <MenuItem onPress={() => {}}>New File</MenuItem>
134
+ <MenuItem onPress={() => {}}>Open File</MenuItem>
135
+ <MenuDivider />
136
+ <MenuItem onPress={() => {}}>Save</MenuItem>
137
+ <MenuItem onPress={() => {}}>Save As</MenuItem>
138
+ <MenuDivider />
139
+ <MenuItem onPress={() => {}}>Export</MenuItem>
140
+ </MenuList>
141
+ </Menu>
142
+ ),
143
+ }
144
+
145
+ export const DisabledItems: Story = {
146
+ render: () => (
147
+ <Menu>
148
+ <MenuTrigger>
149
+ <Button variant="outline" color="primary">
150
+ <ButtonText>Edit</ButtonText>
151
+ </Button>
152
+ </MenuTrigger>
153
+ <MenuList>
154
+ <MenuItem onPress={() => {}}>Undo</MenuItem>
155
+ <MenuItem onPress={() => {}} isDisabled>Redo</MenuItem>
156
+ <MenuDivider />
157
+ <MenuItem onPress={() => {}}>Cut</MenuItem>
158
+ <MenuItem onPress={() => {}}>Copy</MenuItem>
159
+ <MenuItem onPress={() => {}} isDisabled>Paste</MenuItem>
160
+ </MenuList>
161
+ </Menu>
162
+ ),
163
+ }
164
+
165
+ export const Controlled: Story = {
166
+ render: () => (
167
+ <Menu isOpen>
168
+ <MenuTrigger>
169
+ <Button variant="outline" color="primary">
170
+ <ButtonText>Always Open</ButtonText>
171
+ </Button>
172
+ </MenuTrigger>
173
+ <MenuList>
174
+ <MenuItem onPress={() => {}}>Item One</MenuItem>
175
+ <MenuItem onPress={() => {}}>Item Two</MenuItem>
176
+ <MenuItem onPress={() => {}}>Item Three</MenuItem>
177
+ </MenuList>
178
+ </Menu>
179
+ ),
180
+ }
@@ -0,0 +1,353 @@
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
+ Menu,
6
+ MenuTrigger,
7
+ MenuList,
8
+ MenuItem,
9
+ MenuDivider,
10
+ MenuGroup,
11
+ } from './Menu'
12
+
13
+ describe('Menu', () => {
14
+ it('renders trigger element', () => {
15
+ render(
16
+ <Menu>
17
+ <MenuTrigger>
18
+ <button>Options</button>
19
+ </MenuTrigger>
20
+ <MenuList>
21
+ <MenuItem>Edit</MenuItem>
22
+ </MenuList>
23
+ </Menu>
24
+ )
25
+ expect(screen.getByText('Options')).toBeInTheDocument()
26
+ })
27
+
28
+ it('does not show menu list by default', () => {
29
+ render(
30
+ <Menu>
31
+ <MenuTrigger>
32
+ <button>Options</button>
33
+ </MenuTrigger>
34
+ <MenuList>
35
+ <MenuItem>Edit</MenuItem>
36
+ </MenuList>
37
+ </Menu>
38
+ )
39
+ expect(screen.queryByText('Edit')).not.toBeInTheDocument()
40
+ })
41
+
42
+ it('opens menu when trigger is clicked', () => {
43
+ render(
44
+ <Menu>
45
+ <MenuTrigger>
46
+ <button>Options</button>
47
+ </MenuTrigger>
48
+ <MenuList>
49
+ <MenuItem>Edit</MenuItem>
50
+ </MenuList>
51
+ </Menu>
52
+ )
53
+
54
+ fireEvent.click(screen.getByText('Options'))
55
+ expect(screen.getByText('Edit')).toBeInTheDocument()
56
+ })
57
+
58
+ it('closes menu when trigger is clicked again', () => {
59
+ render(
60
+ <Menu>
61
+ <MenuTrigger>
62
+ <button>Options</button>
63
+ </MenuTrigger>
64
+ <MenuList>
65
+ <MenuItem>Edit</MenuItem>
66
+ </MenuList>
67
+ </Menu>
68
+ )
69
+
70
+ fireEvent.click(screen.getByText('Options'))
71
+ expect(screen.getByText('Edit')).toBeInTheDocument()
72
+
73
+ fireEvent.click(screen.getByText('Options'))
74
+ expect(screen.queryByText('Edit')).not.toBeInTheDocument()
75
+ })
76
+
77
+ describe('controlled state', () => {
78
+ it('shows menu when isOpen is true', () => {
79
+ render(
80
+ <Menu isOpen>
81
+ <MenuTrigger>
82
+ <button>Options</button>
83
+ </MenuTrigger>
84
+ <MenuList>
85
+ <MenuItem>Edit</MenuItem>
86
+ </MenuList>
87
+ </Menu>
88
+ )
89
+ expect(screen.getByText('Edit')).toBeInTheDocument()
90
+ })
91
+
92
+ it('hides menu when isOpen is false', () => {
93
+ render(
94
+ <Menu isOpen={false}>
95
+ <MenuTrigger>
96
+ <button>Options</button>
97
+ </MenuTrigger>
98
+ <MenuList>
99
+ <MenuItem>Edit</MenuItem>
100
+ </MenuList>
101
+ </Menu>
102
+ )
103
+ expect(screen.queryByText('Edit')).not.toBeInTheDocument()
104
+ })
105
+
106
+ it('calls onOpenChange when trigger is clicked', () => {
107
+ const onOpenChange = vi.fn()
108
+ render(
109
+ <Menu onOpenChange={onOpenChange}>
110
+ <MenuTrigger>
111
+ <button>Options</button>
112
+ </MenuTrigger>
113
+ <MenuList>
114
+ <MenuItem>Edit</MenuItem>
115
+ </MenuList>
116
+ </Menu>
117
+ )
118
+
119
+ fireEvent.click(screen.getByText('Options'))
120
+ expect(onOpenChange).toHaveBeenCalledWith(true)
121
+ })
122
+ })
123
+
124
+ describe('MenuItem', () => {
125
+ it('calls onPress when clicked', () => {
126
+ const onPress = vi.fn()
127
+ render(
128
+ <Menu isOpen>
129
+ <MenuTrigger>
130
+ <button>Options</button>
131
+ </MenuTrigger>
132
+ <MenuList>
133
+ <MenuItem onPress={onPress}>Edit</MenuItem>
134
+ </MenuList>
135
+ </Menu>
136
+ )
137
+
138
+ fireEvent.click(screen.getByText('Edit'))
139
+ expect(onPress).toHaveBeenCalledTimes(1)
140
+ })
141
+
142
+ it('closes menu after item is clicked', () => {
143
+ render(
144
+ <Menu>
145
+ <MenuTrigger>
146
+ <button>Options</button>
147
+ </MenuTrigger>
148
+ <MenuList>
149
+ <MenuItem onPress={() => {}}>Edit</MenuItem>
150
+ </MenuList>
151
+ </Menu>
152
+ )
153
+
154
+ fireEvent.click(screen.getByText('Options'))
155
+ expect(screen.getByText('Edit')).toBeInTheDocument()
156
+
157
+ fireEvent.click(screen.getByText('Edit'))
158
+ expect(screen.queryByText('Edit')).not.toBeInTheDocument()
159
+ })
160
+
161
+ it('does not call onPress when disabled', () => {
162
+ const onPress = vi.fn()
163
+ render(
164
+ <Menu isOpen>
165
+ <MenuTrigger>
166
+ <button>Options</button>
167
+ </MenuTrigger>
168
+ <MenuList>
169
+ <MenuItem onPress={onPress} isDisabled>
170
+ Disabled Item
171
+ </MenuItem>
172
+ </MenuList>
173
+ </Menu>
174
+ )
175
+
176
+ fireEvent.click(screen.getByText('Disabled Item'))
177
+ expect(onPress).not.toHaveBeenCalled()
178
+ })
179
+
180
+ it('renders destructive item', () => {
181
+ render(
182
+ <Menu isOpen>
183
+ <MenuTrigger>
184
+ <button>Options</button>
185
+ </MenuTrigger>
186
+ <MenuList>
187
+ <MenuItem isDestructive>Delete</MenuItem>
188
+ </MenuList>
189
+ </Menu>
190
+ )
191
+ expect(screen.getByText('Delete')).toBeInTheDocument()
192
+ })
193
+
194
+ it('renders item with icon', () => {
195
+ render(
196
+ <Menu isOpen>
197
+ <MenuTrigger>
198
+ <button>Options</button>
199
+ </MenuTrigger>
200
+ <MenuList>
201
+ <MenuItem icon={<span data-testid="edit-icon">pencil</span>}>
202
+ Edit
203
+ </MenuItem>
204
+ </MenuList>
205
+ </Menu>
206
+ )
207
+ expect(screen.getByTestId('edit-icon')).toBeInTheDocument()
208
+ })
209
+
210
+ it('has menuitem accessibility role', () => {
211
+ render(
212
+ <Menu isOpen>
213
+ <MenuTrigger>
214
+ <button>Options</button>
215
+ </MenuTrigger>
216
+ <MenuList>
217
+ <MenuItem>Edit</MenuItem>
218
+ </MenuList>
219
+ </Menu>
220
+ )
221
+ expect(screen.getByRole('menuitem')).toBeInTheDocument()
222
+ })
223
+ })
224
+
225
+ describe('MenuDivider', () => {
226
+ it('renders a divider', () => {
227
+ const { container } = render(
228
+ <Menu isOpen>
229
+ <MenuTrigger>
230
+ <button>Options</button>
231
+ </MenuTrigger>
232
+ <MenuList>
233
+ <MenuItem>Edit</MenuItem>
234
+ <MenuDivider />
235
+ <MenuItem>Delete</MenuItem>
236
+ </MenuList>
237
+ </Menu>
238
+ )
239
+ expect(container).toBeInTheDocument()
240
+ })
241
+ })
242
+
243
+ describe('MenuGroup', () => {
244
+ it('renders group with label', () => {
245
+ render(
246
+ <Menu isOpen>
247
+ <MenuTrigger>
248
+ <button>Options</button>
249
+ </MenuTrigger>
250
+ <MenuList>
251
+ <MenuGroup label="Actions">
252
+ <MenuItem>Edit</MenuItem>
253
+ <MenuItem>Duplicate</MenuItem>
254
+ </MenuGroup>
255
+ </MenuList>
256
+ </Menu>
257
+ )
258
+ expect(screen.getByText('Actions')).toBeInTheDocument()
259
+ expect(screen.getByText('Edit')).toBeInTheDocument()
260
+ expect(screen.getByText('Duplicate')).toBeInTheDocument()
261
+ })
262
+
263
+ it('renders group without label', () => {
264
+ render(
265
+ <Menu isOpen>
266
+ <MenuTrigger>
267
+ <button>Options</button>
268
+ </MenuTrigger>
269
+ <MenuList>
270
+ <MenuGroup>
271
+ <MenuItem>Edit</MenuItem>
272
+ </MenuGroup>
273
+ </MenuList>
274
+ </Menu>
275
+ )
276
+ expect(screen.getByText('Edit')).toBeInTheDocument()
277
+ })
278
+ })
279
+
280
+ describe('MenuTrigger', () => {
281
+ it('communicates expanded state', () => {
282
+ render(
283
+ <Menu>
284
+ <MenuTrigger>
285
+ <span>Options</span>
286
+ </MenuTrigger>
287
+ <MenuList>
288
+ <MenuItem>Edit</MenuItem>
289
+ </MenuList>
290
+ </Menu>
291
+ )
292
+
293
+ const trigger = screen.getByRole('button', { name: 'Options' })
294
+ // react-native-web does not render aria-expanded for accessibilityState.expanded
295
+ expect(trigger).toBeInTheDocument()
296
+
297
+ fireEvent.click(trigger)
298
+ // Verify menu opens by checking for menu content
299
+ expect(screen.getByRole('menu')).toBeInTheDocument()
300
+ })
301
+ })
302
+
303
+ describe('MenuList', () => {
304
+ it('has menu accessibility role', () => {
305
+ render(
306
+ <Menu isOpen>
307
+ <MenuTrigger>
308
+ <button>Options</button>
309
+ </MenuTrigger>
310
+ <MenuList>
311
+ <MenuItem>Edit</MenuItem>
312
+ </MenuList>
313
+ </Menu>
314
+ )
315
+ expect(screen.getByRole('menu')).toBeInTheDocument()
316
+ })
317
+ })
318
+
319
+ describe('accessibility', () => {
320
+ it('has no accessibility violations when closed', async () => {
321
+ const { container } = render(
322
+ <Menu>
323
+ <MenuTrigger>
324
+ <span>Options</span>
325
+ </MenuTrigger>
326
+ <MenuList>
327
+ <MenuItem>Edit</MenuItem>
328
+ <MenuItem>Delete</MenuItem>
329
+ </MenuList>
330
+ </Menu>
331
+ )
332
+ const results = await axe(container)
333
+ expect(results).toHaveNoViolations()
334
+ })
335
+
336
+ it('has no accessibility violations when open', async () => {
337
+ const { container } = render(
338
+ <Menu isOpen>
339
+ <MenuTrigger>
340
+ <span>Options</span>
341
+ </MenuTrigger>
342
+ <MenuList>
343
+ <MenuItem>Edit</MenuItem>
344
+ <MenuDivider />
345
+ <MenuItem isDestructive>Delete</MenuItem>
346
+ </MenuList>
347
+ </Menu>
348
+ )
349
+ const results = await axe(container)
350
+ expect(results).toHaveNoViolations()
351
+ })
352
+ })
353
+ })