@boostdev/design-system-components 1.2.2 → 1.2.4

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 (215) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +50 -5
  3. package/dist/client.cjs +50 -50
  4. package/dist/client.css +506 -503
  5. package/dist/client.js +50 -50
  6. package/dist/index.cjs +50 -50
  7. package/dist/index.css +506 -503
  8. package/dist/index.js +50 -50
  9. package/dist/native/index.cjs +3692 -352
  10. package/dist/native/index.d.cts +359 -3
  11. package/dist/native/index.d.ts +359 -3
  12. package/dist/native/index.js +3811 -364
  13. package/package.json +3 -3
  14. package/src/components/interaction/Button/Button.module.css +1 -4
  15. package/src/components/interaction/Button/Button.native.stories.tsx +10 -12
  16. package/src/components/interaction/Button/Button.native.tsx +3 -3
  17. package/src/components/interaction/Dialog/Dialog.module.css +1 -1
  18. package/src/components/interaction/Dialog/Dialog.native.mdx +61 -0
  19. package/src/components/interaction/Dialog/Dialog.native.spec.tsx +73 -0
  20. package/src/components/interaction/Dialog/Dialog.native.stories.tsx +53 -0
  21. package/src/components/interaction/Dialog/Dialog.native.tsx +128 -0
  22. package/src/components/interaction/Drawer/Drawer.module.css +1 -1
  23. package/src/components/interaction/Drawer/Drawer.native.mdx +58 -0
  24. package/src/components/interaction/Drawer/Drawer.native.spec.tsx +81 -0
  25. package/src/components/interaction/Drawer/Drawer.native.stories.tsx +33 -0
  26. package/src/components/interaction/Drawer/Drawer.native.tsx +175 -0
  27. package/src/components/interaction/DropdownMenu/DropdownMenu.module.css +4 -0
  28. package/src/components/interaction/DropdownMenu/DropdownMenu.native.mdx +74 -0
  29. package/src/components/interaction/DropdownMenu/DropdownMenu.native.spec.tsx +78 -0
  30. package/src/components/interaction/DropdownMenu/DropdownMenu.native.stories.tsx +51 -0
  31. package/src/components/interaction/DropdownMenu/DropdownMenu.native.tsx +254 -0
  32. package/src/components/interaction/DropdownMenu/DropdownMenu.stories.tsx +4 -4
  33. package/src/components/interaction/Popover/Popover.native.mdx +61 -0
  34. package/src/components/interaction/Popover/Popover.native.spec.tsx +73 -0
  35. package/src/components/interaction/Popover/Popover.native.stories.tsx +44 -0
  36. package/src/components/interaction/Popover/Popover.native.tsx +87 -0
  37. package/src/components/interaction/Rating/Rating.native.mdx +55 -0
  38. package/src/components/interaction/Rating/Rating.native.spec.tsx +38 -0
  39. package/src/components/interaction/Rating/Rating.native.stories.tsx +37 -0
  40. package/src/components/interaction/Rating/Rating.native.tsx +50 -0
  41. package/src/components/interaction/Toast/Toast.native.mdx +81 -0
  42. package/src/components/interaction/Toast/Toast.native.spec.tsx +80 -0
  43. package/src/components/interaction/Toast/Toast.native.stories.tsx +47 -0
  44. package/src/components/interaction/Toast/Toast.native.tsx +202 -0
  45. package/src/components/interaction/form/Checkbox/Checkbox.native.stories.tsx +2 -4
  46. package/src/components/interaction/form/Checkbox/Checkbox.native.tsx +1 -1
  47. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.mdx +78 -0
  48. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.spec.tsx +57 -0
  49. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.stories.tsx +96 -0
  50. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.tsx +61 -0
  51. package/src/components/interaction/form/Combobox/Combobox.module.css +1 -1
  52. package/src/components/interaction/form/Combobox/Combobox.native.mdx +86 -0
  53. package/src/components/interaction/form/Combobox/Combobox.native.spec.tsx +60 -0
  54. package/src/components/interaction/form/Combobox/Combobox.native.stories.tsx +99 -0
  55. package/src/components/interaction/form/Combobox/Combobox.native.tsx +211 -0
  56. package/src/components/interaction/form/FileInput/FileInput.native.mdx +86 -0
  57. package/src/components/interaction/form/FileInput/FileInput.native.spec.tsx +73 -0
  58. package/src/components/interaction/form/FileInput/FileInput.native.stories.tsx +69 -0
  59. package/src/components/interaction/form/FileInput/FileInput.native.tsx +129 -0
  60. package/src/components/interaction/form/FormInput/FormInput.module.css +1 -1
  61. package/src/components/interaction/form/FormInput/FormInput.native.mdx +81 -0
  62. package/src/components/interaction/form/FormInput/FormInput.native.spec.tsx +51 -0
  63. package/src/components/interaction/form/FormInput/FormInput.native.stories.tsx +54 -0
  64. package/src/components/interaction/form/FormInput/FormInput.native.tsx +122 -0
  65. package/src/components/interaction/form/NumberInput/NumberInput.module.css +1 -1
  66. package/src/components/interaction/form/NumberInput/NumberInput.native.mdx +87 -0
  67. package/src/components/interaction/form/NumberInput/NumberInput.native.spec.tsx +63 -0
  68. package/src/components/interaction/form/NumberInput/NumberInput.native.stories.tsx +63 -0
  69. package/src/components/interaction/form/NumberInput/NumberInput.native.tsx +198 -0
  70. package/src/components/interaction/form/Radio/Radio.native.spec.tsx +79 -0
  71. package/src/components/interaction/form/Radio/Radio.native.stories.tsx +12 -14
  72. package/src/components/interaction/form/Radio/Radio.native.tsx +1 -1
  73. package/src/components/interaction/form/RadioGroup/RadioGroup.native.mdx +79 -0
  74. package/src/components/interaction/form/RadioGroup/RadioGroup.native.spec.tsx +66 -0
  75. package/src/components/interaction/form/RadioGroup/RadioGroup.native.stories.tsx +96 -0
  76. package/src/components/interaction/form/RadioGroup/RadioGroup.native.tsx +61 -0
  77. package/src/components/interaction/form/SegmentedControl/SegmentedControl.module.css +5 -5
  78. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.mdx +78 -0
  79. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.spec.tsx +82 -0
  80. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.stories.tsx +99 -0
  81. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.tsx +121 -0
  82. package/src/components/interaction/form/Select/Select.module.css +1 -1
  83. package/src/components/interaction/form/Select/Select.native.mdx +85 -0
  84. package/src/components/interaction/form/Select/Select.native.spec.tsx +64 -0
  85. package/src/components/interaction/form/Select/Select.native.stories.tsx +75 -0
  86. package/src/components/interaction/form/Select/Select.native.tsx +252 -0
  87. package/src/components/interaction/form/Slider/Slider.module.css +4 -4
  88. package/src/components/interaction/form/Slider/Slider.native.mdx +83 -0
  89. package/src/components/interaction/form/Slider/Slider.native.spec.tsx +51 -0
  90. package/src/components/interaction/form/Slider/Slider.native.stories.tsx +58 -0
  91. package/src/components/interaction/form/Slider/Slider.native.tsx +188 -0
  92. package/src/components/interaction/form/Switch/Switch.module.css +3 -3
  93. package/src/components/interaction/form/Switch/Switch.native.stories.tsx +2 -4
  94. package/src/components/interaction/form/Textarea/Textarea.native.mdx +76 -0
  95. package/src/components/interaction/form/Textarea/Textarea.native.spec.tsx +50 -0
  96. package/src/components/interaction/form/Textarea/Textarea.native.stories.tsx +50 -0
  97. package/src/components/interaction/form/Textarea/Textarea.native.tsx +108 -0
  98. package/src/components/interaction/form/atoms/Label.native.spec.tsx +39 -0
  99. package/src/components/interaction/form/atoms/Label.native.stories.tsx +1 -1
  100. package/src/components/interaction/form/atoms/Message.native.stories.tsx +1 -1
  101. package/src/components/layout/ButtonGroup/ButtonGroup.native.spec.tsx +83 -0
  102. package/src/components/layout/ButtonGroup/ButtonGroup.native.stories.tsx +13 -19
  103. package/src/components/layout/Card/Card.native.spec.tsx +102 -0
  104. package/src/components/layout/Card/Card.native.stories.tsx +9 -11
  105. package/src/components/layout/Card/Card.native.tsx +1 -1
  106. package/src/components/layout/IconWrapper/IconWrapper.module.css +1 -1
  107. package/src/components/layout/IconWrapper/IconWrapper.native.stories.tsx +7 -11
  108. package/src/components/layout/SectionHeader/SectionHeader.native.spec.tsx +51 -0
  109. package/src/components/layout/SectionHeader/SectionHeader.native.stories.tsx +6 -8
  110. package/src/components/ui/Accordion/Accordion.native.mdx +65 -0
  111. package/src/components/ui/Accordion/Accordion.native.spec.tsx +69 -0
  112. package/src/components/ui/Accordion/Accordion.native.stories.tsx +52 -0
  113. package/src/components/ui/Accordion/Accordion.native.tsx +141 -0
  114. package/src/components/ui/Alert/Alert.module.css +1 -1
  115. package/src/components/ui/Alert/Alert.native.spec.tsx +76 -0
  116. package/src/components/ui/Alert/Alert.native.stories.tsx +8 -10
  117. package/src/components/ui/Alert/Alert.native.tsx +7 -7
  118. package/src/components/ui/Avatar/Avatar.module.css +1 -1
  119. package/src/components/ui/Avatar/Avatar.native.stories.tsx +6 -8
  120. package/src/components/ui/Avatar/Avatar.native.tsx +2 -2
  121. package/src/components/ui/Badge/Badge.module.css +1 -1
  122. package/src/components/ui/Badge/Badge.native.spec.tsx +45 -0
  123. package/src/components/ui/Badge/Badge.native.stories.tsx +6 -8
  124. package/src/components/ui/Badge/Badge.native.tsx +2 -2
  125. package/src/components/ui/Breadcrumb/Breadcrumb.native.mdx +52 -0
  126. package/src/components/ui/Breadcrumb/Breadcrumb.native.spec.tsx +49 -0
  127. package/src/components/ui/Breadcrumb/Breadcrumb.native.stories.tsx +46 -0
  128. package/src/components/ui/Breadcrumb/Breadcrumb.native.tsx +83 -0
  129. package/src/components/ui/Calendar/Calendar.module.css +2 -2
  130. package/src/components/ui/Calendar/Calendar.native.mdx +58 -0
  131. package/src/components/ui/Calendar/Calendar.native.spec.tsx +92 -0
  132. package/src/components/ui/Calendar/Calendar.native.stories.tsx +45 -0
  133. package/src/components/ui/Calendar/Calendar.native.tsx +294 -0
  134. package/src/components/ui/Carousel/Carousel.module.css +1 -1
  135. package/src/components/ui/Carousel/Carousel.native.mdx +55 -0
  136. package/src/components/ui/Carousel/Carousel.native.spec.tsx +55 -0
  137. package/src/components/ui/Carousel/Carousel.native.stories.tsx +59 -0
  138. package/src/components/ui/Carousel/Carousel.native.tsx +184 -0
  139. package/src/components/ui/Collapsible/Collapsible.native.mdx +53 -0
  140. package/src/components/ui/Collapsible/Collapsible.native.spec.tsx +68 -0
  141. package/src/components/ui/Collapsible/Collapsible.native.stories.tsx +48 -0
  142. package/src/components/ui/Collapsible/Collapsible.native.tsx +139 -0
  143. package/src/components/ui/DescriptionList/DescriptionList.native.mdx +50 -0
  144. package/src/components/ui/DescriptionList/DescriptionList.native.spec.tsx +37 -0
  145. package/src/components/ui/DescriptionList/DescriptionList.native.stories.tsx +48 -0
  146. package/src/components/ui/DescriptionList/DescriptionList.native.tsx +54 -0
  147. package/src/components/ui/Link/Link.native.mdx +53 -0
  148. package/src/components/ui/Link/Link.native.spec.tsx +35 -0
  149. package/src/components/ui/Link/Link.native.stories.tsx +46 -0
  150. package/src/components/ui/Link/Link.native.tsx +48 -0
  151. package/src/components/ui/Loading/Loading.module.css +1 -1
  152. package/src/components/ui/Loading/Loading.native.stories.tsx +6 -8
  153. package/src/components/ui/NotificationBanner/NotificationBanner.module.css +1 -1
  154. package/src/components/ui/NotificationBanner/NotificationBanner.native.spec.tsx +85 -0
  155. package/src/components/ui/NotificationBanner/NotificationBanner.native.stories.tsx +8 -10
  156. package/src/components/ui/NotificationBanner/NotificationBanner.native.tsx +7 -7
  157. package/src/components/ui/Pagination/Pagination.module.css +5 -1
  158. package/src/components/ui/Pagination/Pagination.native.mdx +62 -0
  159. package/src/components/ui/Pagination/Pagination.native.spec.tsx +86 -0
  160. package/src/components/ui/Pagination/Pagination.native.stories.tsx +47 -0
  161. package/src/components/ui/Pagination/Pagination.native.tsx +157 -0
  162. package/src/components/ui/Progress/Progress.module.css +2 -2
  163. package/src/components/ui/Progress/Progress.native.spec.tsx +56 -0
  164. package/src/components/ui/Progress/Progress.native.stories.tsx +1 -1
  165. package/src/components/ui/Progress/Progress.native.tsx +2 -2
  166. package/src/components/ui/ProgressCircle/ProgressCircle.native.mdx +63 -0
  167. package/src/components/ui/ProgressCircle/ProgressCircle.native.spec.tsx +39 -0
  168. package/src/components/ui/ProgressCircle/ProgressCircle.native.stories.tsx +36 -0
  169. package/src/components/ui/ProgressCircle/ProgressCircle.native.tsx +176 -0
  170. package/src/components/ui/Separator/Separator.native.spec.tsx +39 -0
  171. package/src/components/ui/Separator/Separator.native.stories.tsx +10 -14
  172. package/src/components/ui/Skeleton/Skeleton.native.spec.tsx +39 -0
  173. package/src/components/ui/Skeleton/Skeleton.native.stories.tsx +6 -8
  174. package/src/components/ui/Skeleton/Skeleton.native.tsx +2 -1
  175. package/src/components/ui/Table/Table.native.mdx +72 -0
  176. package/src/components/ui/Table/Table.native.spec.tsx +83 -0
  177. package/src/components/ui/Table/Table.native.stories.tsx +55 -0
  178. package/src/components/ui/Table/Table.native.tsx +196 -0
  179. package/src/components/ui/Tabs/Tabs.native.mdx +65 -0
  180. package/src/components/ui/Tabs/Tabs.native.spec.tsx +65 -0
  181. package/src/components/ui/Tabs/Tabs.native.stories.tsx +57 -0
  182. package/src/components/ui/Tabs/Tabs.native.tsx +133 -0
  183. package/src/components/ui/Tooltip/Tooltip.module.css +1 -1
  184. package/src/components/ui/Tooltip/Tooltip.native.mdx +47 -0
  185. package/src/components/ui/Tooltip/Tooltip.native.spec.tsx +37 -0
  186. package/src/components/ui/Tooltip/Tooltip.native.stories.tsx +38 -0
  187. package/src/components/ui/Tooltip/Tooltip.native.tsx +57 -0
  188. package/src/components/ui/Typography/Typography.native.spec.tsx +49 -0
  189. package/src/components/ui/Typography/Typography.native.stories.tsx +8 -10
  190. package/src/native/ThemeContext.tsx +3 -3
  191. package/src/native.ts +37 -0
  192. package/src/stories/ReactNative.mdx +48 -13
  193. package/src/web-components/interaction/BdsAccordion.stories.tsx +1 -1
  194. package/src/web-components/interaction/BdsCollapsible.stories.tsx +1 -1
  195. package/src/web-components/interaction/BdsSkipLink.stories.tsx +1 -1
  196. package/src/web-components/interaction/BdsTabs.stories.tsx +1 -1
  197. package/src/web-components/interaction/BdsTooltip.stories.tsx +1 -1
  198. package/src/web-components/interaction/form/BdsCheckbox.stories.tsx +1 -1
  199. package/src/web-components/interaction/form/BdsCheckboxGroup.stories.tsx +1 -1
  200. package/src/web-components/interaction/form/BdsCombobox.stories.tsx +1 -1
  201. package/src/web-components/interaction/form/BdsFileInput.stories.tsx +1 -1
  202. package/src/web-components/interaction/form/BdsFormInput.stories.tsx +1 -1
  203. package/src/web-components/interaction/form/BdsNumberInput.stories.tsx +1 -1
  204. package/src/web-components/interaction/form/BdsRadio.stories.tsx +1 -1
  205. package/src/web-components/interaction/form/BdsRadioGroup.stories.tsx +1 -1
  206. package/src/web-components/interaction/form/BdsSegmentedControl.stories.tsx +1 -1
  207. package/src/web-components/interaction/form/BdsSelect.stories.tsx +1 -1
  208. package/src/web-components/interaction/form/BdsSlider.stories.tsx +1 -1
  209. package/src/web-components/interaction/form/BdsSwitch.stories.tsx +1 -1
  210. package/src/web-components/interaction/form/BdsTextarea.stories.tsx +1 -1
  211. package/src/web-components/ui/BdsButtonGroup.stories.tsx +1 -1
  212. package/src/web-components/ui/BdsCard.stories.tsx +1 -1
  213. package/src/web-components/ui/BdsIconWrapper.stories.tsx +1 -1
  214. package/src/web-components/ui/BdsRating.stories.tsx +1 -1
  215. package/src/web-components/ui/BdsSectionHeader.stories.tsx +1 -1
@@ -0,0 +1,175 @@
1
+ import { ReactNode, useEffect, useRef } from 'react';
2
+ import {
3
+ Modal,
4
+ View,
5
+ Pressable,
6
+ Text,
7
+ Animated,
8
+ StyleSheet,
9
+ Dimensions,
10
+ StyleProp,
11
+ ViewStyle,
12
+ } from 'react-native';
13
+ import { spacing, font, border } from '../../../native/tokens';
14
+ import { useTheme } from '../../../native/ThemeContext';
15
+
16
+ interface DrawerProps {
17
+ isOpen: boolean;
18
+ onClose: () => void;
19
+ title?: ReactNode;
20
+ children?: ReactNode;
21
+ style?: StyleProp<ViewStyle>;
22
+ }
23
+
24
+ const styles = StyleSheet.create({
25
+ overlay: {
26
+ flex: 1,
27
+ justifyContent: 'flex-end',
28
+ },
29
+ panel: {
30
+ maxHeight: '90%',
31
+ borderTopStartRadius: border.radius.m,
32
+ borderTopEndRadius: border.radius.m,
33
+ paddingTop: spacing.m,
34
+ paddingHorizontal: spacing.m,
35
+ paddingBottom: spacing.m,
36
+ shadowColor: '#000',
37
+ shadowOffset: { width: 0, height: -4 },
38
+ shadowOpacity: 0.15,
39
+ shadowRadius: 16,
40
+ elevation: 16,
41
+ },
42
+ header: {
43
+ flexDirection: 'row',
44
+ justifyContent: 'space-between',
45
+ alignItems: 'center',
46
+ paddingBottom: spacing.m,
47
+ borderBottomWidth: 1,
48
+ marginBottom: spacing.m,
49
+ },
50
+ title: {
51
+ fontFamily: font.family.body,
52
+ fontSize: font.size.heading3,
53
+ fontWeight: font.weight.semibold,
54
+ flex: 1,
55
+ },
56
+ closeButton: {
57
+ width: 44,
58
+ height: 44,
59
+ alignItems: 'center',
60
+ justifyContent: 'center',
61
+ },
62
+ closeText: {
63
+ fontFamily: font.family.body,
64
+ fontSize: 20,
65
+ lineHeight: 24,
66
+ },
67
+ body: {
68
+ flex: 1,
69
+ },
70
+ });
71
+
72
+ export function Drawer({
73
+ isOpen,
74
+ onClose,
75
+ title,
76
+ children,
77
+ style,
78
+ }: Readonly<DrawerProps>) {
79
+ const { colors } = useTheme();
80
+ const screenHeight = Dimensions.get('window').height;
81
+
82
+ const slideAnim = useRef(new Animated.Value(screenHeight)).current;
83
+ const backdropAnim = useRef(new Animated.Value(0)).current;
84
+
85
+ useEffect(() => {
86
+ if (isOpen) {
87
+ Animated.parallel([
88
+ Animated.timing(slideAnim, {
89
+ toValue: 0,
90
+ duration: 250,
91
+ useNativeDriver: true,
92
+ }),
93
+ Animated.timing(backdropAnim, {
94
+ toValue: 1,
95
+ duration: 250,
96
+ useNativeDriver: true,
97
+ }),
98
+ ]).start();
99
+ } else {
100
+ Animated.parallel([
101
+ Animated.timing(slideAnim, {
102
+ toValue: screenHeight,
103
+ duration: 200,
104
+ useNativeDriver: true,
105
+ }),
106
+ Animated.timing(backdropAnim, {
107
+ toValue: 0,
108
+ duration: 200,
109
+ useNativeDriver: true,
110
+ }),
111
+ ]).start();
112
+ }
113
+ }, [isOpen, slideAnim, backdropAnim, screenHeight]);
114
+
115
+ return (
116
+ <Modal
117
+ visible={isOpen}
118
+ transparent
119
+ animationType="none"
120
+ onRequestClose={onClose}
121
+ >
122
+ <View style={styles.overlay}>
123
+ <Animated.View
124
+ style={[
125
+ StyleSheet.absoluteFill,
126
+ { backgroundColor: 'rgba(0, 0, 0, 0.5)', opacity: backdropAnim },
127
+ ]}
128
+ >
129
+ <Pressable
130
+ style={StyleSheet.absoluteFill}
131
+ onPress={onClose}
132
+ accessibilityRole="button"
133
+ accessibilityLabel="Close drawer"
134
+ />
135
+ </Animated.View>
136
+ <Animated.View
137
+ style={[
138
+ styles.panel,
139
+ {
140
+ backgroundColor: colors.colorBg,
141
+ transform: [{ translateY: slideAnim }],
142
+ },
143
+ style,
144
+ ]}
145
+ accessibilityRole="none"
146
+ >
147
+ <View
148
+ style={[styles.header, { borderBottomColor: colors.colorBgSubtle }]}
149
+ >
150
+ {!!title && (
151
+ <Text style={[styles.title, { color: colors.colorOnBg }]}>
152
+ {title}
153
+ </Text>
154
+ )}
155
+ <Pressable
156
+ style={styles.closeButton}
157
+ onPress={onClose}
158
+ accessibilityRole="button"
159
+ accessibilityLabel="Close drawer"
160
+ hitSlop={8}
161
+ >
162
+ <Text
163
+ style={[styles.closeText, { color: colors.colorOnBgSubtle }]}
164
+ aria-hidden
165
+ >
166
+ {'\u2715'}
167
+ </Text>
168
+ </Pressable>
169
+ </View>
170
+ <View style={styles.body}>{children}</View>
171
+ </Animated.View>
172
+ </View>
173
+ </Modal>
174
+ );
175
+ }
@@ -22,6 +22,10 @@
22
22
  margin: 0;
23
23
  }
24
24
 
25
+ .menu li {
26
+ list-style: none;
27
+ }
28
+
25
29
  .--placement_bottom-start { inset-inline-start: 0; }
26
30
  .--placement_bottom-end { inset-inline-end: 0; }
27
31
 
@@ -0,0 +1,74 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './DropdownMenu.native.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # DropdownMenu (React Native)
7
+
8
+ A dropdown menu that opens as a bottom sheet modal on mobile. Accepts a trigger element and a list of menu items.
9
+
10
+ > **Status: alpha** — API may change before stable release.
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { DropdownMenu } from '@boostdev/components';
16
+
17
+ <DropdownMenu
18
+ trigger={<Button>Actions</Button>}
19
+ title="Actions"
20
+ items={[
21
+ { id: 'edit', label: 'Edit', onPress: () => console.log('edit') },
22
+ { id: 'delete', label: 'Delete', onPress: () => console.log('delete') },
23
+ ]}
24
+ />
25
+ ```
26
+
27
+ Wrap your tree in `ThemeProvider` (once, at the app root):
28
+
29
+ ```tsx
30
+ import { ThemeProvider } from '@boostdev/components/native/ThemeContext';
31
+
32
+ export default function App() {
33
+ return <ThemeProvider><YourApp /></ThemeProvider>;
34
+ }
35
+ ```
36
+
37
+ ## Examples
38
+
39
+ ### Default
40
+ <Canvas of={Stories.Default} />
41
+
42
+ ### With disabled items
43
+ <Canvas of={Stories.WithDisabledItems} />
44
+
45
+ ## Props
46
+
47
+ <ArgTypes of={Stories} />
48
+
49
+ ## Props reference
50
+
51
+ | Prop | Type | Default | Description |
52
+ |------|------|---------|-------------|
53
+ | `trigger` | `ReactElement` | — | Element that opens the menu on press |
54
+ | `items` | `DropdownMenuItem[]` | — | Array of menu items to display |
55
+ | `title` | `string` | `'Menu'` | Title displayed in the sheet header |
56
+ | `style` | `StyleProp<ViewStyle>` | — | Override wrapper styles |
57
+
58
+ ### DropdownMenuItem
59
+
60
+ | Property | Type | Default | Description |
61
+ |----------|------|---------|-------------|
62
+ | `id` | `string` | — | Unique identifier |
63
+ | `label` | `string` | — | Display text |
64
+ | `onPress` | `() => void` | — | Called when item is pressed |
65
+ | `disabled` | `boolean` | `false` | Prevents interaction |
66
+ | `icon` | `ReactNode` | — | Optional icon before the label |
67
+ | `separator` | `boolean` | `false` | Renders a separator line above the item |
68
+
69
+ ## Accessibility
70
+
71
+ - Trigger has `accessibilityRole="button"` with `expanded` state
72
+ - Menu items have `accessibilityRole="menuitem"`
73
+ - Disabled items communicate their state to assistive technologies
74
+ - Android back button closes the menu
@@ -0,0 +1,78 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { Text, Pressable } from 'react-native';
3
+ import { ThemeProvider } from '../../../native/ThemeContext';
4
+ import { DropdownMenu } from './DropdownMenu.native';
5
+
6
+ function renderWithTheme(ui: React.ReactElement) {
7
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
8
+ }
9
+
10
+ function Trigger({ children, ...rest }: { children: string; onPress?: () => void }) {
11
+ return <Pressable {...rest}><Text>{children}</Text></Pressable>;
12
+ }
13
+
14
+ const items = [
15
+ { id: 'edit', label: 'Edit' },
16
+ { id: 'delete', label: 'Delete' },
17
+ ];
18
+
19
+ const itemsWithDisabled = [
20
+ { id: 'view', label: 'View' },
21
+ { id: 'share', label: 'Share', disabled: true },
22
+ ];
23
+
24
+ describe('DropdownMenu (native)', () => {
25
+ it('renders the trigger', () => {
26
+ renderWithTheme(
27
+ <DropdownMenu trigger={<Trigger>Open</Trigger>} items={items} />,
28
+ );
29
+ expect(screen.getByText('Open')).toBeInTheDocument();
30
+ });
31
+
32
+ it('opens the menu on trigger press', () => {
33
+ renderWithTheme(
34
+ <DropdownMenu trigger={<Trigger>Menu</Trigger>} items={items} title="Actions" />,
35
+ );
36
+ fireEvent.click(screen.getByRole('button', { name: 'Actions' }));
37
+ expect(screen.getByText('Edit')).toBeInTheDocument();
38
+ expect(screen.getByText('Delete')).toBeInTheDocument();
39
+ });
40
+
41
+ it('calls item onPress and closes menu when item is pressed', () => {
42
+ const onEdit = vi.fn();
43
+ const menuItems = [
44
+ { id: 'edit', label: 'Edit', onPress: onEdit },
45
+ { id: 'delete', label: 'Delete' },
46
+ ];
47
+ renderWithTheme(
48
+ <DropdownMenu trigger={<Trigger>Actions</Trigger>} items={menuItems} title="Actions" />,
49
+ );
50
+ fireEvent.click(screen.getByRole('button', { name: 'Actions' }));
51
+ fireEvent.click(screen.getByRole('menuitem', { name: 'Edit' }));
52
+ expect(onEdit).toHaveBeenCalledTimes(1);
53
+ });
54
+
55
+ it('renders disabled items with disabled state', () => {
56
+ renderWithTheme(
57
+ <DropdownMenu trigger={<Trigger>Options</Trigger>} items={itemsWithDisabled} title="Options" />,
58
+ );
59
+ fireEvent.click(screen.getByRole('button', { name: 'Options' }));
60
+ expect(screen.getByRole('menuitem', { name: 'Share' })).toHaveAttribute('aria-disabled', 'true');
61
+ });
62
+
63
+ it('renders a close button in the sheet header', () => {
64
+ renderWithTheme(
65
+ <DropdownMenu trigger={<Trigger>Menu</Trigger>} items={items} title="Menu" />,
66
+ );
67
+ fireEvent.click(screen.getByRole('button', { name: 'Menu' }));
68
+ expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument();
69
+ });
70
+
71
+ it('renders the title in the sheet header', () => {
72
+ renderWithTheme(
73
+ <DropdownMenu trigger={<Trigger>Trigger</Trigger>} items={items} title="My Menu" />,
74
+ );
75
+ fireEvent.click(screen.getByRole('button', { name: 'My Menu' }));
76
+ expect(screen.getByText('My Menu')).toBeInTheDocument();
77
+ });
78
+ });
@@ -0,0 +1,51 @@
1
+ import React from 'react';
2
+ import { Text } from 'react-native';
3
+ import type { Meta, StoryObj } from '@storybook/react';
4
+ import { ThemeProvider } from '../../../native/ThemeContext';
5
+ import { DropdownMenu } from './DropdownMenu.native';
6
+ import { Button } from '../Button/Button.native';
7
+
8
+ const meta = {
9
+ tags: ['!stable', 'alpha'],
10
+ title: 'React Native/Interaction/DropdownMenu',
11
+ component: DropdownMenu,
12
+ decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
13
+ parameters: { layout: 'centered' },
14
+ } satisfies Meta<typeof DropdownMenu>;
15
+
16
+ export default meta;
17
+ type Story = StoryObj<typeof meta>;
18
+
19
+ const defaultItems = [
20
+ { id: 'edit', label: 'Edit' },
21
+ { id: 'duplicate', label: 'Duplicate' },
22
+ { id: 'archive', label: 'Archive', separator: true },
23
+ { id: 'delete', label: 'Delete' },
24
+ ];
25
+
26
+ const withDisabledItems = [
27
+ { id: 'view', label: 'View details' },
28
+ { id: 'edit', label: 'Edit' },
29
+ { id: 'share', label: 'Share', disabled: true },
30
+ { id: 'delete', label: 'Delete', separator: true },
31
+ ];
32
+
33
+ export const Default: Story = {
34
+ render: () => (
35
+ <DropdownMenu
36
+ trigger={<Button>Actions</Button>}
37
+ items={defaultItems}
38
+ title="Actions"
39
+ />
40
+ ),
41
+ };
42
+
43
+ export const WithDisabledItems: Story = {
44
+ render: () => (
45
+ <DropdownMenu
46
+ trigger={<Text>{'More \u2026'}</Text>}
47
+ items={withDisabledItems}
48
+ title="Options"
49
+ />
50
+ ),
51
+ };
@@ -0,0 +1,254 @@
1
+ import React, { ReactElement, ReactNode, useState, useEffect, useRef } from 'react';
2
+ import {
3
+ Modal,
4
+ View,
5
+ Pressable,
6
+ Text,
7
+ FlatList,
8
+ Animated,
9
+ StyleSheet,
10
+ Dimensions,
11
+ SafeAreaView,
12
+ StyleProp,
13
+ ViewStyle,
14
+ } from 'react-native';
15
+ import { spacing, font, border, lineHeight } from '../../../native/tokens';
16
+ import { useTheme } from '../../../native/ThemeContext';
17
+
18
+ export interface DropdownMenuItem {
19
+ id: string;
20
+ label: string;
21
+ onPress?: () => void;
22
+ disabled?: boolean;
23
+ icon?: ReactNode;
24
+ separator?: boolean;
25
+ }
26
+
27
+ interface DropdownMenuProps {
28
+ trigger: ReactElement;
29
+ items: DropdownMenuItem[];
30
+ title?: string;
31
+ style?: StyleProp<ViewStyle>;
32
+ }
33
+
34
+ const styles = StyleSheet.create({
35
+ overlay: {
36
+ flex: 1,
37
+ justifyContent: 'flex-end',
38
+ },
39
+ sheet: {
40
+ maxHeight: '60%',
41
+ borderTopStartRadius: border.radius.m,
42
+ borderTopEndRadius: border.radius.m,
43
+ paddingTop: spacing.m,
44
+ paddingBottom: spacing.xxxl,
45
+ },
46
+ sheetHeader: {
47
+ flexDirection: 'row',
48
+ justifyContent: 'space-between',
49
+ alignItems: 'center',
50
+ paddingHorizontal: spacing.m,
51
+ paddingBottom: spacing.s,
52
+ borderBottomWidth: 1,
53
+ marginBottom: spacing.xxs,
54
+ },
55
+ sheetTitle: {
56
+ fontFamily: font.family.body,
57
+ fontSize: font.size.heading3,
58
+ fontWeight: font.weight.semibold,
59
+ },
60
+ closeText: {
61
+ fontFamily: font.family.body,
62
+ fontSize: font.size.body,
63
+ fontWeight: font.weight.semibold,
64
+ },
65
+ separator: {
66
+ height: 1,
67
+ marginHorizontal: spacing.m,
68
+ marginVertical: spacing.xxs,
69
+ },
70
+ item: {
71
+ flexDirection: 'row',
72
+ alignItems: 'center',
73
+ paddingHorizontal: spacing.m,
74
+ paddingVertical: spacing.s,
75
+ minHeight: 48,
76
+ gap: spacing.s,
77
+ },
78
+ itemText: {
79
+ fontFamily: font.family.body,
80
+ fontSize: font.size.body,
81
+ lineHeight: lineHeight.body,
82
+ },
83
+ });
84
+
85
+ export function DropdownMenu({
86
+ trigger,
87
+ items,
88
+ title = 'Menu',
89
+ style,
90
+ }: Readonly<DropdownMenuProps>) {
91
+ const { colors } = useTheme();
92
+ const [open, setOpen] = useState(false);
93
+ const screenHeight = Dimensions.get('window').height;
94
+
95
+ const slideAnim = useRef(new Animated.Value(screenHeight)).current;
96
+ const backdropAnim = useRef(new Animated.Value(0)).current;
97
+
98
+ useEffect(() => {
99
+ if (open) {
100
+ Animated.parallel([
101
+ Animated.timing(slideAnim, {
102
+ toValue: 0,
103
+ duration: 250,
104
+ useNativeDriver: true,
105
+ }),
106
+ Animated.timing(backdropAnim, {
107
+ toValue: 1,
108
+ duration: 250,
109
+ useNativeDriver: true,
110
+ }),
111
+ ]).start();
112
+ } else {
113
+ Animated.parallel([
114
+ Animated.timing(slideAnim, {
115
+ toValue: screenHeight,
116
+ duration: 200,
117
+ useNativeDriver: true,
118
+ }),
119
+ Animated.timing(backdropAnim, {
120
+ toValue: 0,
121
+ duration: 200,
122
+ useNativeDriver: true,
123
+ }),
124
+ ]).start();
125
+ }
126
+ }, [open, slideAnim, backdropAnim, screenHeight]);
127
+
128
+ const handleItemPress = (item: DropdownMenuItem) => {
129
+ item.onPress?.();
130
+ setOpen(false);
131
+ };
132
+
133
+ return (
134
+ <View style={style}>
135
+ {React.cloneElement(trigger as ReactElement<Record<string, unknown>>, {
136
+ onPress: () => setOpen(true),
137
+ accessibilityRole: 'button',
138
+ accessibilityLabel: title,
139
+ })}
140
+
141
+ <Modal
142
+ visible={open}
143
+ transparent
144
+ animationType="none"
145
+ onRequestClose={() => setOpen(false)}
146
+ >
147
+ <View style={styles.overlay}>
148
+ <Animated.View
149
+ style={[
150
+ StyleSheet.absoluteFill,
151
+ { backgroundColor: 'rgba(0, 0, 0, 0.5)', opacity: backdropAnim },
152
+ ]}
153
+ >
154
+ <Pressable
155
+ style={StyleSheet.absoluteFill}
156
+ onPress={() => setOpen(false)}
157
+ accessibilityRole="button"
158
+ accessibilityLabel="Close menu"
159
+ />
160
+ </Animated.View>
161
+ <Animated.View
162
+ style={[
163
+ styles.sheet,
164
+ {
165
+ backgroundColor: colors.colorBg,
166
+ transform: [{ translateY: slideAnim }],
167
+ },
168
+ ]}
169
+ >
170
+ <Pressable
171
+ onPress={() => {
172
+ /* prevent close when pressing sheet */
173
+ }}
174
+ >
175
+ <SafeAreaView>
176
+ <View
177
+ style={[
178
+ styles.sheetHeader,
179
+ { borderBottomColor: colors.colorBgSubtle },
180
+ ]}
181
+ >
182
+ <Text style={[styles.sheetTitle, { color: colors.colorOnBg }]}>
183
+ {title}
184
+ </Text>
185
+ <Pressable
186
+ accessibilityRole="button"
187
+ accessibilityLabel="Close"
188
+ onPress={() => setOpen(false)}
189
+ hitSlop={8}
190
+ >
191
+ <Text
192
+ style={[
193
+ styles.closeText,
194
+ { color: colors.colorInteractive },
195
+ ]}
196
+ >
197
+ Done
198
+ </Text>
199
+ </Pressable>
200
+ </View>
201
+ <FlatList
202
+ data={items}
203
+ keyExtractor={(item) => item.id}
204
+ renderItem={({ item }) => (
205
+ <>
206
+ {item.separator && (
207
+ <View
208
+ style={[
209
+ styles.separator,
210
+ { backgroundColor: colors.colorBgSubtle },
211
+ ]}
212
+ accessibilityRole="none"
213
+ />
214
+ )}
215
+ <Pressable
216
+ accessibilityRole="menuitem"
217
+ accessibilityLabel={item.label}
218
+ accessibilityState={{ disabled: item.disabled }}
219
+ disabled={item.disabled}
220
+ onPress={() => handleItemPress(item)}
221
+ style={({ pressed }) => [
222
+ styles.item,
223
+ pressed && { backgroundColor: colors.colorBgSubtle },
224
+ item.disabled && { opacity: 0.4 },
225
+ ]}
226
+ >
227
+ {!!item.icon && (
228
+ <View
229
+ importantForAccessibility="no-hide-descendants"
230
+ accessibilityElementsHidden={true}
231
+ >
232
+ {item.icon}
233
+ </View>
234
+ )}
235
+ <Text
236
+ style={[
237
+ styles.itemText,
238
+ { color: colors.colorOnBg },
239
+ ]}
240
+ >
241
+ {item.label}
242
+ </Text>
243
+ </Pressable>
244
+ </>
245
+ )}
246
+ />
247
+ </SafeAreaView>
248
+ </Pressable>
249
+ </Animated.View>
250
+ </View>
251
+ </Modal>
252
+ </View>
253
+ );
254
+ }
@@ -26,14 +26,14 @@ export const Default: Story = {
26
26
  ],
27
27
  },
28
28
  render: args => (
29
- <DropdownMenu {...args} trigger={<Button variant="ghost">Actions ▾</Button>} />
29
+ <DropdownMenu {...args} trigger={<Button variant="outline">Actions ▾</Button>} />
30
30
  ),
31
31
  };
32
32
 
33
33
  export const WithDisabledItem: Story = {
34
34
  render: () => (
35
35
  <DropdownMenu
36
- trigger={<Button variant="ghost">Options ▾</Button>}
36
+ trigger={<Button variant="outline">Options ▾</Button>}
37
37
  items={[
38
38
  { id: 'view', label: 'View' },
39
39
  { id: 'edit', label: 'Edit' },
@@ -46,7 +46,7 @@ export const WithDisabledItem: Story = {
46
46
  export const WithSeparator: Story = {
47
47
  render: () => (
48
48
  <DropdownMenu
49
- trigger={<Button variant="ghost">File ▾</Button>}
49
+ trigger={<Button variant="outline">File ▾</Button>}
50
50
  items={[
51
51
  { id: 'new', label: 'New' },
52
52
  { id: 'open', label: 'Open' },
@@ -60,7 +60,7 @@ export const WithSeparator: Story = {
60
60
  export const AlignedEnd: Story = {
61
61
  render: () => (
62
62
  <DropdownMenu
63
- trigger={<Button variant="ghost">⋮</Button>}
63
+ trigger={<Button variant="outline">⋮</Button>}
64
64
  items={[{ id: 'copy', label: 'Copy link' }, { id: 'report', label: 'Report' }]}
65
65
  placement="bottom-end"
66
66
  />