@utilitywarehouse/hearth-react-native 0.8.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-lint.log +1 -1
  3. package/CHANGELOG.md +16 -0
  4. package/build/components/Banner/Banner.js +25 -6
  5. package/build/components/Banner/Banner.props.d.ts +2 -2
  6. package/build/components/BottomSheet/BottomSheetHandle.js +8 -0
  7. package/build/components/Menu/Menu.context.d.ts +5 -0
  8. package/build/components/Menu/Menu.context.js +9 -0
  9. package/build/components/Menu/Menu.d.ts +4 -0
  10. package/build/components/Menu/Menu.js +25 -0
  11. package/build/components/Menu/Menu.props.d.ts +21 -0
  12. package/build/components/Menu/Menu.props.js +1 -0
  13. package/build/components/Menu/MenuItem.d.ts +18 -0
  14. package/build/components/Menu/MenuItem.js +115 -0
  15. package/build/components/Menu/MenuItem.props.d.ts +27 -0
  16. package/build/components/Menu/MenuItem.props.js +1 -0
  17. package/build/components/Menu/MenuTrigger.d.ts +9 -0
  18. package/build/components/Menu/MenuTrigger.js +11 -0
  19. package/build/components/Menu/MenuTrigger.props.d.ts +12 -0
  20. package/build/components/Menu/MenuTrigger.props.js +1 -0
  21. package/build/components/Menu/index.d.ts +7 -0
  22. package/build/components/Menu/index.js +4 -0
  23. package/build/components/Modal/Modal.d.ts +1 -1
  24. package/build/components/Modal/Modal.js +32 -30
  25. package/build/components/Modal/Modal.props.d.ts +1 -0
  26. package/build/components/Modal/Modal.web.d.ts +1 -1
  27. package/build/components/Modal/Modal.web.js +25 -25
  28. package/build/components/PillGroup/Pill.d.ts +16 -0
  29. package/build/components/PillGroup/Pill.js +94 -0
  30. package/build/components/PillGroup/Pill.props.d.ts +10 -0
  31. package/build/components/PillGroup/Pill.props.js +1 -0
  32. package/build/components/PillGroup/PillGroup.context.d.ts +6 -0
  33. package/build/components/PillGroup/PillGroup.context.js +5 -0
  34. package/build/components/PillGroup/PillGroup.d.ts +5 -0
  35. package/build/components/PillGroup/PillGroup.js +34 -0
  36. package/build/components/PillGroup/PillGroup.props.d.ts +15 -0
  37. package/build/components/PillGroup/PillGroup.props.js +1 -0
  38. package/build/components/PillGroup/index.d.ts +4 -0
  39. package/build/components/PillGroup/index.js +2 -0
  40. package/build/components/Select/Select.js +2 -1
  41. package/build/components/Toast/Toast.context.d.ts +9 -0
  42. package/build/components/Toast/Toast.context.js +90 -0
  43. package/build/components/Toast/Toast.props.d.ts +29 -0
  44. package/build/components/Toast/Toast.props.js +1 -0
  45. package/build/components/Toast/ToastItem.d.ts +10 -0
  46. package/build/components/Toast/ToastItem.js +129 -0
  47. package/build/components/Toast/index.d.ts +3 -0
  48. package/build/components/Toast/index.js +2 -0
  49. package/build/components/index.d.ts +3 -0
  50. package/build/components/index.js +3 -0
  51. package/build/tokens/components/dark/checkbox.d.ts +3 -0
  52. package/build/tokens/components/dark/checkbox.js +3 -0
  53. package/build/tokens/components/dark/index.d.ts +3 -1
  54. package/build/tokens/components/dark/index.js +3 -1
  55. package/build/tokens/components/dark/input.d.ts +9 -0
  56. package/build/tokens/components/dark/input.js +9 -0
  57. package/build/tokens/components/dark/modal.d.ts +7 -4
  58. package/build/tokens/components/dark/modal.js +7 -4
  59. package/build/tokens/components/dark/radio.d.ts +3 -0
  60. package/build/tokens/components/dark/radio.js +3 -0
  61. package/build/tokens/components/dark/rating.d.ts +8 -0
  62. package/build/tokens/components/dark/rating.js +7 -0
  63. package/build/tokens/components/dark/table.d.ts +2 -3
  64. package/build/tokens/components/dark/table.js +2 -3
  65. package/build/tokens/components/dark/time-picker.d.ts +29 -0
  66. package/build/tokens/components/dark/time-picker.js +28 -0
  67. package/build/tokens/components/dark/timeline.d.ts +27 -0
  68. package/build/tokens/components/dark/timeline.js +26 -0
  69. package/build/tokens/components/dark/toast.d.ts +6 -2
  70. package/build/tokens/components/dark/toast.js +6 -2
  71. package/build/tokens/components/light/checkbox.d.ts +3 -0
  72. package/build/tokens/components/light/checkbox.js +3 -0
  73. package/build/tokens/components/light/index.d.ts +3 -1
  74. package/build/tokens/components/light/index.js +3 -1
  75. package/build/tokens/components/light/input.d.ts +9 -0
  76. package/build/tokens/components/light/input.js +9 -0
  77. package/build/tokens/components/light/modal.d.ts +7 -4
  78. package/build/tokens/components/light/modal.js +7 -4
  79. package/build/tokens/components/light/radio.d.ts +3 -0
  80. package/build/tokens/components/light/radio.js +3 -0
  81. package/build/tokens/components/light/rating.d.ts +8 -0
  82. package/build/tokens/components/light/rating.js +7 -0
  83. package/build/tokens/components/light/table.d.ts +2 -3
  84. package/build/tokens/components/light/table.js +2 -3
  85. package/build/tokens/components/light/time-picker.d.ts +29 -0
  86. package/build/tokens/components/light/time-picker.js +28 -0
  87. package/build/tokens/components/light/timeline.d.ts +27 -0
  88. package/build/tokens/components/light/timeline.js +26 -0
  89. package/build/tokens/components/light/toast.d.ts +6 -2
  90. package/build/tokens/components/light/toast.js +6 -2
  91. package/docs/assets/toast-ios.MP4 +0 -0
  92. package/docs/components/AllComponents.web.tsx +59 -0
  93. package/docs/components/BackToTopButton.tsx +1 -1
  94. package/package.json +4 -4
  95. package/src/components/Banner/Banner.docs.mdx +19 -10
  96. package/src/components/Banner/Banner.props.ts +2 -2
  97. package/src/components/Banner/Banner.stories.tsx +1 -4
  98. package/src/components/Banner/Banner.tsx +47 -7
  99. package/src/components/BottomSheet/BottomSheetHandle.tsx +12 -0
  100. package/src/components/DatePickerInput/DatePickerInput.docs.mdx +1 -1
  101. package/src/components/Menu/Menu.context.ts +15 -0
  102. package/src/components/Menu/Menu.docs.mdx +158 -0
  103. package/src/components/Menu/Menu.props.ts +24 -0
  104. package/src/components/Menu/Menu.stories.tsx +292 -0
  105. package/src/components/Menu/Menu.tsx +54 -0
  106. package/src/components/Menu/MenuItem.props.ts +29 -0
  107. package/src/components/Menu/MenuItem.tsx +145 -0
  108. package/src/components/Menu/MenuTrigger.props.ts +14 -0
  109. package/src/components/Menu/MenuTrigger.tsx +20 -0
  110. package/src/components/Menu/index.ts +7 -0
  111. package/src/components/Modal/Modal.docs.mdx +34 -5
  112. package/src/components/Modal/Modal.props.ts +1 -0
  113. package/src/components/Modal/Modal.stories.tsx +46 -0
  114. package/src/components/Modal/Modal.tsx +37 -33
  115. package/src/components/Modal/Modal.web.tsx +27 -27
  116. package/src/components/PillGroup/Pill.props.ts +13 -0
  117. package/src/components/PillGroup/Pill.tsx +120 -0
  118. package/src/components/PillGroup/PillGroup.context.tsx +12 -0
  119. package/src/components/PillGroup/PillGroup.docs.mdx +96 -0
  120. package/src/components/PillGroup/PillGroup.props.ts +22 -0
  121. package/src/components/PillGroup/PillGroup.stories.tsx +159 -0
  122. package/src/components/PillGroup/PillGroup.tsx +66 -0
  123. package/src/components/PillGroup/index.ts +4 -0
  124. package/src/components/Select/Select.tsx +2 -0
  125. package/src/components/Toast/Toast.context.tsx +118 -0
  126. package/src/components/Toast/Toast.docs.mdx +164 -0
  127. package/src/components/Toast/Toast.props.ts +33 -0
  128. package/src/components/Toast/Toast.stories.tsx +356 -0
  129. package/src/components/Toast/ToastItem.tsx +200 -0
  130. package/src/components/Toast/index.ts +3 -0
  131. package/src/components/index.ts +3 -0
  132. package/src/tokens/components/dark/checkbox.ts +3 -0
  133. package/src/tokens/components/dark/index.ts +3 -1
  134. package/src/tokens/components/dark/input.ts +9 -0
  135. package/src/tokens/components/dark/modal.ts +7 -4
  136. package/src/tokens/components/dark/radio.ts +3 -0
  137. package/src/tokens/components/dark/rating.ts +8 -0
  138. package/src/tokens/components/dark/table.ts +2 -3
  139. package/src/tokens/components/dark/time-picker.ts +29 -0
  140. package/src/tokens/components/dark/timeline.ts +27 -0
  141. package/src/tokens/components/dark/toast.ts +6 -2
  142. package/src/tokens/components/light/checkbox.ts +3 -0
  143. package/src/tokens/components/light/index.ts +3 -1
  144. package/src/tokens/components/light/input.ts +9 -0
  145. package/src/tokens/components/light/modal.ts +7 -4
  146. package/src/tokens/components/light/radio.ts +3 -0
  147. package/src/tokens/components/light/rating.ts +8 -0
  148. package/src/tokens/components/light/table.ts +2 -3
  149. package/src/tokens/components/light/time-picker.ts +29 -0
  150. package/src/tokens/components/light/timeline.ts +27 -0
  151. package/src/tokens/components/light/toast.ts +6 -2
  152. package/build/tokens/components/dark/dialog.d.ts +0 -25
  153. package/build/tokens/components/dark/dialog.js +0 -24
  154. package/build/tokens/components/light/dialog.d.ts +0 -25
  155. package/build/tokens/components/light/dialog.js +0 -24
  156. package/src/tokens/components/dark/dialog.ts +0 -25
  157. package/src/tokens/components/light/dialog.ts +0 -25
@@ -0,0 +1,158 @@
1
+ import { Canvas, Controls, Meta, Story } from '@storybook/addon-docs/blocks';
2
+ import { BackToTopButton, ViewFigmaButton } from '../../../docs/components';
3
+ import * as Stories from './Menu.stories';
4
+
5
+ <Meta title="Components / Menu" />
6
+
7
+ <ViewFigmaButton url="https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens" />
8
+
9
+ <BackToTopButton />
10
+
11
+ # Menu
12
+
13
+ The `Menu` component provides a bottom sheet modal with a list of actions. It's perfect for contextual menus, action sheets, and option lists. The component uses `BottomSheetModal` under the hood and includes `MenuItem` components that can display icons, text, and support different color schemes.
14
+
15
+ - [Playground](#playground)
16
+ - [Usage](#usage)
17
+ - [Props](#props)
18
+ - [Features](#features)
19
+ - [Examples](#examples)
20
+ - [Basic Menu](#basic-menu)
21
+ - [With Destructive Action](#with-destructive-action)
22
+ - [Icon on Right](#icon-on-right)
23
+ - [Without Icons](#without-icons)
24
+ - [With Disabled Items](#with-disabled-items)
25
+ - [Without Heading](#without-heading)
26
+ - [Accessibility](#accessibility)
27
+
28
+ ## Playground
29
+
30
+ <Canvas of={Stories.Playground} />
31
+
32
+ <Controls of={Stories.Playground} />
33
+
34
+ ## Usage
35
+
36
+ ### Basic Usage
37
+
38
+ ```tsx
39
+ import { useRef } from 'react';
40
+ import { Menu, MenuItem, MenuTrigger } from '@utilitywarehouse/hearth-react-native';
41
+ import { EditSmallIcon, TrashSmallIcon } from '@utilitywarehouse/hearth-react-native-icons';
42
+
43
+ const MyComponent = () => {
44
+ const menuRef = useRef(null);
45
+
46
+ return (
47
+ <>
48
+ <MenuTrigger onPress={() => menuRef.current?.present()}>
49
+ <Button>Open Menu</Button>
50
+ </MenuTrigger>
51
+
52
+ <Menu ref={menuRef} heading="Actions">
53
+ <MenuItem icon={EditSmallIcon} text="Edit" onPress={() => console.log('Edit')} />
54
+ <MenuItem
55
+ icon={TrashSmallIcon}
56
+ text="Delete"
57
+ colorScheme="destructive"
58
+ onPress={() => console.log('Delete')}
59
+ />
60
+ </Menu>
61
+ </>
62
+ );
63
+ };
64
+ ```
65
+
66
+ ## Props
67
+
68
+ ### Menu
69
+
70
+ | Prop | Type | Default | Description |
71
+ | ------------------ | -------------------------------- | ------- | --------------------------------------------------- |
72
+ | `heading` | `string` | - | Heading text displayed at the top of the menu |
73
+ | `children` | `ReactNode` | - | Menu items to display |
74
+ | `bottomSheetProps` | `Partial<BottomSheetModalProps>` | - | Optional bottom sheet modal props for customization |
75
+
76
+ ### MenuItem
77
+
78
+ | Prop | Type | Default | Description |
79
+ | -------------- | ---------------------------------------- | -------------- | ---------------------------------- |
80
+ | `icon` | `ComponentType` | - | Icon component to display |
81
+ | `iconPosition` | `'left' \| 'right'` | `'left'` | Position of the icon |
82
+ | `text` | `string` | - | Text to display in the menu item |
83
+ | `colorScheme` | `'functional' \| 'destructive'` | `'functional'` | Color scheme for the menu item |
84
+ | `disabled` | `boolean` | `false` | Whether the menu item is disabled |
85
+ | `onPress` | `(event: GestureResponderEvent) => void` | - | Callback when menu item is pressed |
86
+
87
+ ### MenuTrigger
88
+
89
+ | Prop | Type | Description |
90
+ | ---------- | -------------- | ------------------------------------------------------------------- |
91
+ | `children` | `ReactElement` | The child element that triggers the menu (must be a single element) |
92
+ | `onPress` | `() => void` | Function that opens the menu |
93
+
94
+ ## Features
95
+
96
+ ### Icon Positioning
97
+
98
+ Icons can be positioned on either the left or right side of the menu item text using the `iconPosition` prop.
99
+
100
+ ### Color Schemes
101
+
102
+ Menu items support two color schemes:
103
+
104
+ - `functional` (default): Uses primary text and icon colors
105
+ - `destructive`: Uses danger/destructive colors for actions like delete
106
+
107
+ ### Disabled State
108
+
109
+ Menu items can be disabled using the `disabled` prop, which applies reduced opacity and prevents interaction.
110
+
111
+ ### Auto-closing
112
+
113
+ Menu items automatically close the menu when pressed, making it easy to handle actions without manual menu dismissal.
114
+
115
+ ## Examples
116
+
117
+ ### Basic Menu
118
+
119
+ A simple menu with icon-based actions:
120
+
121
+ <Canvas of={Stories.BasicMenu} />
122
+
123
+ ### With Destructive Action
124
+
125
+ Menu with a destructive action styled differently:
126
+
127
+ <Canvas of={Stories.WithDestructiveAction} />
128
+
129
+ ### Icon on Right
130
+
131
+ Menu items with icons positioned on the right side:
132
+
133
+ <Canvas of={Stories.IconOnRight} />
134
+
135
+ ### Without Icons
136
+
137
+ Simple text-only menu items:
138
+
139
+ <Canvas of={Stories.WithoutIcons} />
140
+
141
+ ### With Disabled Items
142
+
143
+ Menu with some items disabled:
144
+
145
+ <Canvas of={Stories.WithDisabledItems} />
146
+
147
+ ### Without Heading
148
+
149
+ Menu without a heading section:
150
+
151
+ <Canvas of={Stories.WithoutHeading} />
152
+
153
+ ## Accessibility
154
+
155
+ - Menu items have `accessibilityRole="button"` for proper screen reader support
156
+ - Disabled state is communicated via `accessibilityState`
157
+ - Keyboard navigation is supported on web platforms
158
+ - All interactive elements are properly focusable
@@ -0,0 +1,24 @@
1
+ import type { BottomSheetModalProps } from '@gorhom/bottom-sheet';
2
+ import type { ReactNode } from 'react';
3
+
4
+ export interface MenuMethods {
5
+ present: () => void;
6
+ dismiss: () => void;
7
+ }
8
+
9
+ export interface MenuProps {
10
+ /**
11
+ * Heading text displayed at the top of the menu
12
+ */
13
+ heading?: string;
14
+ /**
15
+ * Menu items to display
16
+ */
17
+ children: ReactNode;
18
+ /**
19
+ * Optional bottom sheet modal props to customise the menu behavior
20
+ */
21
+ bottomSheetProps?: Partial<BottomSheetModalProps>;
22
+ }
23
+
24
+ export default MenuProps;
@@ -0,0 +1,292 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-native';
2
+ import {
3
+ DownloadSmallIcon,
4
+ EditSmallIcon,
5
+ SettingsMediumIcon,
6
+ ShareSmallIcon,
7
+ TickSmallIcon,
8
+ TrashSmallIcon,
9
+ } from '@utilitywarehouse/hearth-react-native-icons';
10
+ import { useRef } from 'react';
11
+ import { Platform, View } from 'react-native';
12
+ import { Button } from '../Button';
13
+ import { Menu, MenuItem, MenuTrigger } from './';
14
+ import type { MenuMethods } from './Menu.props';
15
+
16
+ import { ViewWrap } from '../../../docs/components';
17
+
18
+ const meta: Meta<typeof Menu> = {
19
+ title: 'Stories / Menu',
20
+ component: Menu,
21
+ parameters: {
22
+ layout: 'centered',
23
+ noScroll: true,
24
+ },
25
+ argTypes: {},
26
+ args: {
27
+ heading: 'Menu Options',
28
+ },
29
+ };
30
+
31
+ export default meta;
32
+ type Story = StoryObj<typeof meta>;
33
+
34
+ export const Playground: Story = {
35
+ render: ({ ...args }) => {
36
+ const menuRef = useRef<MenuMethods>(null);
37
+
38
+ const openMenu = () => {
39
+ menuRef.current?.present();
40
+ };
41
+
42
+ return (
43
+ <View style={Platform.OS === 'web' ? { width: 400, height: 400 } : { flex: 1 }}>
44
+ <ViewWrap>
45
+ <MenuTrigger onPress={openMenu}>
46
+ <Button>Open Menu</Button>
47
+ </MenuTrigger>
48
+
49
+ <Menu ref={menuRef} {...args}>
50
+ <MenuItem
51
+ icon={EditSmallIcon}
52
+ text="Edit"
53
+ onPress={() => console.log('Edit pressed')}
54
+ />
55
+ <MenuItem
56
+ icon={ShareSmallIcon}
57
+ text="Share"
58
+ onPress={() => console.log('Share pressed')}
59
+ />
60
+ <MenuItem
61
+ icon={DownloadSmallIcon}
62
+ text="Download"
63
+ onPress={() => console.log('Download pressed')}
64
+ />
65
+ <MenuItem
66
+ icon={TrashSmallIcon}
67
+ text="Delete"
68
+ colorScheme="destructive"
69
+ onPress={() => console.log('Delete pressed')}
70
+ />
71
+ </Menu>
72
+ </ViewWrap>
73
+ </View>
74
+ );
75
+ },
76
+ };
77
+
78
+ export const BasicMenu = () => {
79
+ const menuRef = useRef<MenuMethods>(null);
80
+
81
+ const openMenu = () => {
82
+ menuRef.current?.present();
83
+ };
84
+
85
+ return (
86
+ <View style={Platform.OS === 'web' ? { width: 400, height: 400 } : { flex: 1 }}>
87
+ <ViewWrap>
88
+ <MenuTrigger onPress={openMenu}>
89
+ <Button>Actions</Button>
90
+ </MenuTrigger>
91
+
92
+ <Menu ref={menuRef} heading="Actions">
93
+ <MenuItem icon={EditSmallIcon} text="Edit" onPress={() => console.log('Edit pressed')} />
94
+ <MenuItem
95
+ icon={ShareSmallIcon}
96
+ text="Share"
97
+ onPress={() => console.log('Share pressed')}
98
+ />
99
+ <MenuItem
100
+ icon={DownloadSmallIcon}
101
+ text="Download"
102
+ onPress={() => console.log('Download pressed')}
103
+ />
104
+ </Menu>
105
+ </ViewWrap>
106
+ </View>
107
+ );
108
+ };
109
+
110
+ export const WithDestructiveAction = () => {
111
+ const menuRef = useRef<MenuMethods>(null);
112
+
113
+ const openMenu = () => {
114
+ menuRef.current?.present();
115
+ };
116
+
117
+ return (
118
+ <View style={Platform.OS === 'web' ? { width: 400, height: 400 } : {}}>
119
+ <ViewWrap>
120
+ <MenuTrigger onPress={openMenu}>
121
+ <Button>File Options</Button>
122
+ </MenuTrigger>
123
+
124
+ <Menu ref={menuRef} heading="File Options">
125
+ <MenuItem
126
+ icon={EditSmallIcon}
127
+ text="Rename"
128
+ onPress={() => console.log('Rename pressed')}
129
+ />
130
+ <MenuItem
131
+ icon={ShareSmallIcon}
132
+ text="Share"
133
+ onPress={() => console.log('Share pressed')}
134
+ />
135
+ <MenuItem
136
+ icon={DownloadSmallIcon}
137
+ text="Download"
138
+ onPress={() => console.log('Download pressed')}
139
+ />
140
+ <MenuItem
141
+ icon={TrashSmallIcon}
142
+ text="Delete"
143
+ colorScheme="destructive"
144
+ onPress={() => console.log('Delete pressed')}
145
+ />
146
+ </Menu>
147
+ </ViewWrap>
148
+ </View>
149
+ );
150
+ };
151
+
152
+ export const IconOnRight = () => {
153
+ const menuRef = useRef<MenuMethods>(null);
154
+
155
+ const openMenu = () => {
156
+ menuRef.current?.present();
157
+ };
158
+
159
+ return (
160
+ <View style={Platform.OS === 'web' ? { width: 400, height: 400 } : {}}>
161
+ <ViewWrap>
162
+ <MenuTrigger onPress={openMenu}>
163
+ <Button>Settings</Button>
164
+ </MenuTrigger>
165
+
166
+ <Menu ref={menuRef} heading="Settings">
167
+ <MenuItem
168
+ icon={TickSmallIcon}
169
+ iconPosition="right"
170
+ text="Enable Notifications"
171
+ onPress={() => console.log('Toggle notifications')}
172
+ />
173
+ <MenuItem
174
+ icon={TickSmallIcon}
175
+ iconPosition="right"
176
+ text="Dark Mode"
177
+ onPress={() => console.log('Toggle dark mode')}
178
+ />
179
+ <MenuItem
180
+ icon={SettingsMediumIcon}
181
+ iconPosition="right"
182
+ text="Advanced Settings"
183
+ onPress={() => console.log('Open advanced settings')}
184
+ />
185
+ </Menu>
186
+ </ViewWrap>
187
+ </View>
188
+ );
189
+ };
190
+
191
+ export const WithoutIcons = () => {
192
+ const menuRef = useRef<MenuMethods>(null);
193
+
194
+ const openMenu = () => {
195
+ menuRef.current?.present();
196
+ };
197
+
198
+ return (
199
+ <View style={Platform.OS === 'web' ? { width: 400, height: 400 } : {}}>
200
+ <ViewWrap>
201
+ <MenuTrigger onPress={openMenu}>
202
+ <Button>Sort By</Button>
203
+ </MenuTrigger>
204
+
205
+ <Menu ref={menuRef} heading="Sort By">
206
+ <MenuItem text="Name" onPress={() => console.log('Sort by name')} />
207
+ <MenuItem text="Date" onPress={() => console.log('Sort by date')} />
208
+ <MenuItem text="Size" onPress={() => console.log('Sort by size')} />
209
+ <MenuItem text="Type" onPress={() => console.log('Sort by type')} />
210
+ </Menu>
211
+ </ViewWrap>
212
+ </View>
213
+ );
214
+ };
215
+
216
+ export const WithDisabledItems = () => {
217
+ const menuRef = useRef<MenuMethods>(null);
218
+
219
+ const openMenu = () => {
220
+ menuRef.current?.present();
221
+ };
222
+
223
+ return (
224
+ <View style={Platform.OS === 'web' ? { width: 400, height: 400 } : {}}>
225
+ <ViewWrap>
226
+ <MenuTrigger onPress={openMenu}>
227
+ <Button>Document Actions</Button>
228
+ </MenuTrigger>
229
+
230
+ <Menu ref={menuRef} heading="Document Actions">
231
+ <MenuItem icon={EditSmallIcon} text="Edit" onPress={() => console.log('Edit pressed')} />
232
+ <MenuItem
233
+ icon={ShareSmallIcon}
234
+ text="Share"
235
+ disabled
236
+ onPress={() => console.log('Share pressed')}
237
+ />
238
+ <MenuItem
239
+ icon={DownloadSmallIcon}
240
+ text="Download"
241
+ disabled
242
+ onPress={() => console.log('Download pressed')}
243
+ />
244
+ <MenuItem
245
+ icon={TrashSmallIcon}
246
+ text="Delete"
247
+ colorScheme="destructive"
248
+ onPress={() => console.log('Delete pressed')}
249
+ />
250
+ </Menu>
251
+ </ViewWrap>
252
+ </View>
253
+ );
254
+ };
255
+
256
+ export const WithoutHeading = () => {
257
+ const menuRef = useRef<MenuMethods>(null);
258
+
259
+ const openMenu = () => {
260
+ menuRef.current?.present();
261
+ };
262
+
263
+ return (
264
+ <View style={Platform.OS === 'web' ? { width: 400, height: 400 } : {}}>
265
+ <ViewWrap>
266
+ <MenuTrigger onPress={openMenu}>
267
+ <Button>Quick Actions</Button>
268
+ </MenuTrigger>
269
+
270
+ <Menu ref={menuRef}>
271
+ <MenuItem icon={EditSmallIcon} text="Edit" onPress={() => console.log('Edit pressed')} />
272
+ <MenuItem
273
+ icon={ShareSmallIcon}
274
+ text="Share"
275
+ onPress={() => console.log('Share pressed')}
276
+ />
277
+ <MenuItem
278
+ icon={DownloadSmallIcon}
279
+ text="Download"
280
+ onPress={() => console.log('Download pressed')}
281
+ />
282
+ <MenuItem
283
+ icon={TrashSmallIcon}
284
+ text="Delete"
285
+ colorScheme="destructive"
286
+ onPress={() => console.log('Delete pressed')}
287
+ />
288
+ </Menu>
289
+ </ViewWrap>
290
+ </View>
291
+ );
292
+ };
@@ -0,0 +1,54 @@
1
+ import { forwardRef, useCallback, useImperativeHandle, useMemo, useRef } from 'react';
2
+ import { StyleSheet } from 'react-native-unistyles';
3
+ import { BodyText } from '../BodyText';
4
+ import { BottomSheetModal, BottomSheetProps, BottomSheetScrollView } from '../BottomSheet';
5
+ import { MenuContext } from './Menu.context';
6
+ import type MenuProps from './Menu.props';
7
+ import type { MenuMethods } from './Menu.props';
8
+
9
+ const Menu = forwardRef<MenuMethods, MenuProps>(({ heading, children, bottomSheetProps }, ref) => {
10
+ const bottomSheetModalRef = useRef<BottomSheetModal>(null);
11
+
12
+ useImperativeHandle(
13
+ ref,
14
+ () => ({
15
+ present: () => bottomSheetModalRef.current?.present(),
16
+ dismiss: () => bottomSheetModalRef.current?.dismiss(),
17
+ }),
18
+ []
19
+ );
20
+
21
+ const handleClose = useCallback(() => {
22
+ bottomSheetModalRef.current?.dismiss();
23
+ }, []);
24
+
25
+ const contextValue = useMemo(() => ({ close: handleClose }), [handleClose]);
26
+
27
+ return (
28
+ <BottomSheetModal
29
+ ref={bottomSheetModalRef}
30
+ {...(bottomSheetProps as Partial<BottomSheetProps>)}
31
+ >
32
+ <BottomSheetScrollView contentContainerStyle={styles.container}>
33
+ <MenuContext.Provider value={contextValue}>
34
+ {heading && (
35
+ <BodyText size="md" weight="semibold">
36
+ {heading}
37
+ </BodyText>
38
+ )}
39
+ {children}
40
+ </MenuContext.Provider>
41
+ </BottomSheetScrollView>
42
+ </BottomSheetModal>
43
+ );
44
+ });
45
+
46
+ Menu.displayName = 'Menu';
47
+
48
+ const styles = StyleSheet.create(theme => ({
49
+ container: {
50
+ gap: theme.components.bottomSheet.gap,
51
+ },
52
+ }));
53
+
54
+ export default Menu;
@@ -0,0 +1,29 @@
1
+ import type { ComponentType } from 'react';
2
+ import type { PressableProps } from 'react-native';
3
+
4
+ export interface MenuItemProps extends Omit<PressableProps, 'children'> {
5
+ /**
6
+ * Icon component to display
7
+ */
8
+ icon?: ComponentType;
9
+ /**
10
+ * Position of the icon
11
+ * @default 'left'
12
+ */
13
+ iconPosition?: 'left' | 'right';
14
+ /**
15
+ * Text to display in the menu item
16
+ */
17
+ text: string;
18
+ /**
19
+ * Color scheme for the menu item
20
+ * @default 'functional'
21
+ */
22
+ colorScheme?: 'functional' | 'destructive';
23
+ /**
24
+ * Whether the menu item is disabled
25
+ */
26
+ disabled?: boolean;
27
+ }
28
+
29
+ export default MenuItemProps;
@@ -0,0 +1,145 @@
1
+ import { createPressable } from '@gluestack-ui/pressable';
2
+ import { Pressable } from 'react-native';
3
+ import { StyleSheet } from 'react-native-unistyles';
4
+ import { BodyText } from '../BodyText';
5
+ import { Icon } from '../Icon';
6
+ import { useMenuContext } from './Menu.context';
7
+ import type MenuItemProps from './MenuItem.props';
8
+
9
+ const MenuItemRoot = ({
10
+ icon,
11
+ iconPosition = 'left',
12
+ text,
13
+ colorScheme = 'functional',
14
+ disabled = false,
15
+ onPress,
16
+ states = {},
17
+ ...props
18
+ }: MenuItemProps & { states?: { active?: boolean; disabled?: boolean } }) => {
19
+ const { active } = states;
20
+ const { close } = useMenuContext();
21
+
22
+ styles.useVariants({ colorScheme, disabled, iconPosition, active });
23
+
24
+ const handlePress = (event: any) => {
25
+ if (disabled) return;
26
+ onPress?.(event);
27
+ close();
28
+ };
29
+
30
+ return (
31
+ <Pressable
32
+ {...props}
33
+ onPress={handlePress}
34
+ disabled={disabled}
35
+ style={styles.container}
36
+ accessibilityRole="button"
37
+ accessibilityState={{ disabled }}
38
+ >
39
+ {!!icon && <Icon as={icon} style={styles.icon} />}
40
+ <BodyText size="lg" style={styles.text}>
41
+ {text}
42
+ </BodyText>
43
+ </Pressable>
44
+ );
45
+ };
46
+
47
+ const MenuItem = createPressable({ Root: MenuItemRoot });
48
+
49
+ MenuItem.displayName = 'MenuItem';
50
+
51
+ const styles = StyleSheet.create(theme => ({
52
+ container: {
53
+ flexDirection: 'row',
54
+ alignItems: 'center',
55
+ paddingVertical: theme.components.menu.item.padding,
56
+ paddingHorizontal: theme.components.menu.mobile.item.padding,
57
+ gap: theme.components.menu.item.gap,
58
+ borderRadius: theme.components.menu.item.borderRadius,
59
+ variants: {
60
+ active: {
61
+ true: {
62
+ backgroundColor: theme.color.interactive.functional.surface.subtle.active,
63
+ },
64
+ },
65
+ disabled: {
66
+ true: {
67
+ opacity: theme.opacity.disabled,
68
+ cursor: 'auto',
69
+ },
70
+ false: {
71
+ _web: {
72
+ _hover: {
73
+ backgroundColor: theme.color.interactive.functional.surface.subtle.hover,
74
+ },
75
+ _active: {
76
+ backgroundColor: theme.color.interactive.functional.surface.subtle.active,
77
+ },
78
+ },
79
+ },
80
+ },
81
+ iconPosition: {
82
+ left: {
83
+ flexDirection: 'row',
84
+ },
85
+ right: {
86
+ flexDirection: 'row-reverse',
87
+ },
88
+ },
89
+ colorScheme: {
90
+ functional: {},
91
+ destructive: {},
92
+ },
93
+ },
94
+ compoundVariants: [
95
+ {
96
+ colorScheme: 'destructive',
97
+ active: true,
98
+ styles: {
99
+ backgroundColor: theme.color.interactive.destructive.surface.subtle.active,
100
+ },
101
+ },
102
+ {
103
+ colorScheme: 'destructive',
104
+ disabled: false,
105
+ styles: {
106
+ _web: {
107
+ _hover: {
108
+ backgroundColor: theme.color.interactive.destructive.surface.subtle.hover,
109
+ },
110
+ _active: {
111
+ backgroundColor: theme.color.interactive.destructive.surface.subtle.active,
112
+ },
113
+ },
114
+ },
115
+ },
116
+ ],
117
+ },
118
+ text: {
119
+ flex: 1,
120
+ variants: {
121
+ colorScheme: {
122
+ functional: {
123
+ color: theme.color.text.primary,
124
+ },
125
+ destructive: {
126
+ color: theme.color.interactive.destructive.foreground.subtle,
127
+ },
128
+ },
129
+ },
130
+ },
131
+ icon: {
132
+ variants: {
133
+ colorScheme: {
134
+ functional: {
135
+ color: theme.color.icon.primary,
136
+ },
137
+ destructive: {
138
+ color: theme.color.interactive.destructive.foreground.subtle,
139
+ },
140
+ },
141
+ },
142
+ },
143
+ }));
144
+
145
+ export default MenuItem;