@boostdev/design-system-components 1.2.3 → 1.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 (198) hide show
  1. package/AGENTS.md +19 -11
  2. package/README.md +36 -1
  3. package/dist/client.cjs +50 -50
  4. package/dist/client.css +470 -470
  5. package/dist/client.js +50 -50
  6. package/dist/index.cjs +50 -50
  7. package/dist/index.css +470 -470
  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/dist/utils.cjs +30 -0
  14. package/dist/utils.d.cts +8 -0
  15. package/dist/utils.d.ts +8 -0
  16. package/dist/utils.js +5 -0
  17. package/package.json +10 -4
  18. package/src/client.ts +6 -0
  19. package/src/components/interaction/Button/Button.native.stories.tsx +10 -12
  20. package/src/components/interaction/Button/Button.native.tsx +3 -3
  21. package/src/components/interaction/Dialog/Dialog.native.mdx +61 -0
  22. package/src/components/interaction/Dialog/Dialog.native.spec.tsx +73 -0
  23. package/src/components/interaction/Dialog/Dialog.native.stories.tsx +53 -0
  24. package/src/components/interaction/Dialog/Dialog.native.tsx +128 -0
  25. package/src/components/interaction/Drawer/Drawer.native.mdx +58 -0
  26. package/src/components/interaction/Drawer/Drawer.native.spec.tsx +81 -0
  27. package/src/components/interaction/Drawer/Drawer.native.stories.tsx +33 -0
  28. package/src/components/interaction/Drawer/Drawer.native.tsx +175 -0
  29. package/src/components/interaction/DropdownMenu/DropdownMenu.native.mdx +74 -0
  30. package/src/components/interaction/DropdownMenu/DropdownMenu.native.spec.tsx +78 -0
  31. package/src/components/interaction/DropdownMenu/DropdownMenu.native.stories.tsx +51 -0
  32. package/src/components/interaction/DropdownMenu/DropdownMenu.native.tsx +254 -0
  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.native.mdx +86 -0
  52. package/src/components/interaction/form/Combobox/Combobox.native.spec.tsx +60 -0
  53. package/src/components/interaction/form/Combobox/Combobox.native.stories.tsx +99 -0
  54. package/src/components/interaction/form/Combobox/Combobox.native.tsx +211 -0
  55. package/src/components/interaction/form/FileInput/FileInput.native.mdx +86 -0
  56. package/src/components/interaction/form/FileInput/FileInput.native.spec.tsx +73 -0
  57. package/src/components/interaction/form/FileInput/FileInput.native.stories.tsx +69 -0
  58. package/src/components/interaction/form/FileInput/FileInput.native.tsx +129 -0
  59. package/src/components/interaction/form/FormInput/FormInput.native.mdx +81 -0
  60. package/src/components/interaction/form/FormInput/FormInput.native.spec.tsx +51 -0
  61. package/src/components/interaction/form/FormInput/FormInput.native.stories.tsx +54 -0
  62. package/src/components/interaction/form/FormInput/FormInput.native.tsx +122 -0
  63. package/src/components/interaction/form/NumberInput/NumberInput.native.mdx +87 -0
  64. package/src/components/interaction/form/NumberInput/NumberInput.native.spec.tsx +63 -0
  65. package/src/components/interaction/form/NumberInput/NumberInput.native.stories.tsx +63 -0
  66. package/src/components/interaction/form/NumberInput/NumberInput.native.tsx +198 -0
  67. package/src/components/interaction/form/Radio/Radio.native.spec.tsx +79 -0
  68. package/src/components/interaction/form/Radio/Radio.native.stories.tsx +12 -14
  69. package/src/components/interaction/form/Radio/Radio.native.tsx +1 -1
  70. package/src/components/interaction/form/RadioGroup/RadioGroup.native.mdx +79 -0
  71. package/src/components/interaction/form/RadioGroup/RadioGroup.native.spec.tsx +66 -0
  72. package/src/components/interaction/form/RadioGroup/RadioGroup.native.stories.tsx +96 -0
  73. package/src/components/interaction/form/RadioGroup/RadioGroup.native.tsx +61 -0
  74. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.mdx +78 -0
  75. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.spec.tsx +82 -0
  76. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.stories.tsx +99 -0
  77. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.tsx +121 -0
  78. package/src/components/interaction/form/Select/Select.native.mdx +85 -0
  79. package/src/components/interaction/form/Select/Select.native.spec.tsx +64 -0
  80. package/src/components/interaction/form/Select/Select.native.stories.tsx +75 -0
  81. package/src/components/interaction/form/Select/Select.native.tsx +252 -0
  82. package/src/components/interaction/form/Slider/Slider.native.mdx +83 -0
  83. package/src/components/interaction/form/Slider/Slider.native.spec.tsx +51 -0
  84. package/src/components/interaction/form/Slider/Slider.native.stories.tsx +58 -0
  85. package/src/components/interaction/form/Slider/Slider.native.tsx +188 -0
  86. package/src/components/interaction/form/Switch/Switch.native.stories.tsx +2 -4
  87. package/src/components/interaction/form/Textarea/Textarea.native.mdx +76 -0
  88. package/src/components/interaction/form/Textarea/Textarea.native.spec.tsx +50 -0
  89. package/src/components/interaction/form/Textarea/Textarea.native.stories.tsx +50 -0
  90. package/src/components/interaction/form/Textarea/Textarea.native.tsx +108 -0
  91. package/src/components/interaction/form/atoms/Label.native.spec.tsx +39 -0
  92. package/src/components/interaction/form/atoms/Label.native.stories.tsx +1 -1
  93. package/src/components/interaction/form/atoms/Message.native.stories.tsx +1 -1
  94. package/src/components/layout/ButtonGroup/ButtonGroup.native.spec.tsx +83 -0
  95. package/src/components/layout/ButtonGroup/ButtonGroup.native.stories.tsx +13 -19
  96. package/src/components/layout/Card/Card.native.spec.tsx +102 -0
  97. package/src/components/layout/Card/Card.native.stories.tsx +9 -11
  98. package/src/components/layout/Card/Card.native.tsx +1 -1
  99. package/src/components/layout/IconWrapper/IconWrapper.native.stories.tsx +7 -11
  100. package/src/components/layout/SectionHeader/SectionHeader.native.spec.tsx +51 -0
  101. package/src/components/layout/SectionHeader/SectionHeader.native.stories.tsx +6 -8
  102. package/src/components/ui/Accordion/Accordion.native.mdx +65 -0
  103. package/src/components/ui/Accordion/Accordion.native.spec.tsx +69 -0
  104. package/src/components/ui/Accordion/Accordion.native.stories.tsx +52 -0
  105. package/src/components/ui/Accordion/Accordion.native.tsx +141 -0
  106. package/src/components/ui/Alert/Alert.native.spec.tsx +76 -0
  107. package/src/components/ui/Alert/Alert.native.stories.tsx +8 -10
  108. package/src/components/ui/Alert/Alert.native.tsx +7 -7
  109. package/src/components/ui/Avatar/Avatar.native.stories.tsx +6 -8
  110. package/src/components/ui/Avatar/Avatar.native.tsx +2 -2
  111. package/src/components/ui/Badge/Badge.native.spec.tsx +45 -0
  112. package/src/components/ui/Badge/Badge.native.stories.tsx +6 -8
  113. package/src/components/ui/Badge/Badge.native.tsx +2 -2
  114. package/src/components/ui/Breadcrumb/Breadcrumb.native.mdx +52 -0
  115. package/src/components/ui/Breadcrumb/Breadcrumb.native.spec.tsx +49 -0
  116. package/src/components/ui/Breadcrumb/Breadcrumb.native.stories.tsx +46 -0
  117. package/src/components/ui/Breadcrumb/Breadcrumb.native.tsx +83 -0
  118. package/src/components/ui/Calendar/Calendar.native.mdx +58 -0
  119. package/src/components/ui/Calendar/Calendar.native.spec.tsx +92 -0
  120. package/src/components/ui/Calendar/Calendar.native.stories.tsx +45 -0
  121. package/src/components/ui/Calendar/Calendar.native.tsx +294 -0
  122. package/src/components/ui/Carousel/Carousel.native.mdx +55 -0
  123. package/src/components/ui/Carousel/Carousel.native.spec.tsx +55 -0
  124. package/src/components/ui/Carousel/Carousel.native.stories.tsx +59 -0
  125. package/src/components/ui/Carousel/Carousel.native.tsx +184 -0
  126. package/src/components/ui/Collapsible/Collapsible.native.mdx +53 -0
  127. package/src/components/ui/Collapsible/Collapsible.native.spec.tsx +68 -0
  128. package/src/components/ui/Collapsible/Collapsible.native.stories.tsx +48 -0
  129. package/src/components/ui/Collapsible/Collapsible.native.tsx +139 -0
  130. package/src/components/ui/DescriptionList/DescriptionList.native.mdx +50 -0
  131. package/src/components/ui/DescriptionList/DescriptionList.native.spec.tsx +37 -0
  132. package/src/components/ui/DescriptionList/DescriptionList.native.stories.tsx +48 -0
  133. package/src/components/ui/DescriptionList/DescriptionList.native.tsx +54 -0
  134. package/src/components/ui/Link/Link.native.mdx +53 -0
  135. package/src/components/ui/Link/Link.native.spec.tsx +35 -0
  136. package/src/components/ui/Link/Link.native.stories.tsx +46 -0
  137. package/src/components/ui/Link/Link.native.tsx +48 -0
  138. package/src/components/ui/Loading/Loading.native.stories.tsx +6 -8
  139. package/src/components/ui/NotificationBanner/NotificationBanner.native.spec.tsx +85 -0
  140. package/src/components/ui/NotificationBanner/NotificationBanner.native.stories.tsx +8 -10
  141. package/src/components/ui/NotificationBanner/NotificationBanner.native.tsx +7 -7
  142. package/src/components/ui/Pagination/Pagination.native.mdx +62 -0
  143. package/src/components/ui/Pagination/Pagination.native.spec.tsx +86 -0
  144. package/src/components/ui/Pagination/Pagination.native.stories.tsx +47 -0
  145. package/src/components/ui/Pagination/Pagination.native.tsx +157 -0
  146. package/src/components/ui/Progress/Progress.native.spec.tsx +56 -0
  147. package/src/components/ui/Progress/Progress.native.stories.tsx +1 -1
  148. package/src/components/ui/Progress/Progress.native.tsx +2 -2
  149. package/src/components/ui/ProgressCircle/ProgressCircle.native.mdx +63 -0
  150. package/src/components/ui/ProgressCircle/ProgressCircle.native.spec.tsx +39 -0
  151. package/src/components/ui/ProgressCircle/ProgressCircle.native.stories.tsx +36 -0
  152. package/src/components/ui/ProgressCircle/ProgressCircle.native.tsx +176 -0
  153. package/src/components/ui/Separator/Separator.native.spec.tsx +39 -0
  154. package/src/components/ui/Separator/Separator.native.stories.tsx +10 -14
  155. package/src/components/ui/Skeleton/Skeleton.native.spec.tsx +39 -0
  156. package/src/components/ui/Skeleton/Skeleton.native.stories.tsx +6 -8
  157. package/src/components/ui/Skeleton/Skeleton.native.tsx +2 -1
  158. package/src/components/ui/Table/Table.native.mdx +72 -0
  159. package/src/components/ui/Table/Table.native.spec.tsx +83 -0
  160. package/src/components/ui/Table/Table.native.stories.tsx +55 -0
  161. package/src/components/ui/Table/Table.native.tsx +196 -0
  162. package/src/components/ui/Tabs/Tabs.native.mdx +65 -0
  163. package/src/components/ui/Tabs/Tabs.native.spec.tsx +65 -0
  164. package/src/components/ui/Tabs/Tabs.native.stories.tsx +57 -0
  165. package/src/components/ui/Tabs/Tabs.native.tsx +133 -0
  166. package/src/components/ui/Tooltip/Tooltip.native.mdx +47 -0
  167. package/src/components/ui/Tooltip/Tooltip.native.spec.tsx +37 -0
  168. package/src/components/ui/Tooltip/Tooltip.native.stories.tsx +38 -0
  169. package/src/components/ui/Tooltip/Tooltip.native.tsx +57 -0
  170. package/src/components/ui/Typography/Typography.native.spec.tsx +49 -0
  171. package/src/components/ui/Typography/Typography.native.stories.tsx +8 -10
  172. package/src/native/ThemeContext.tsx +3 -3
  173. package/src/native.ts +37 -0
  174. package/src/stories/ReactNative.mdx +48 -13
  175. package/src/utils.ts +6 -0
  176. package/src/web-components/interaction/BdsAccordion.stories.tsx +1 -1
  177. package/src/web-components/interaction/BdsCollapsible.stories.tsx +1 -1
  178. package/src/web-components/interaction/BdsSkipLink.stories.tsx +1 -1
  179. package/src/web-components/interaction/BdsTabs.stories.tsx +1 -1
  180. package/src/web-components/interaction/BdsTooltip.stories.tsx +1 -1
  181. package/src/web-components/interaction/form/BdsCheckbox.stories.tsx +1 -1
  182. package/src/web-components/interaction/form/BdsCheckboxGroup.stories.tsx +1 -1
  183. package/src/web-components/interaction/form/BdsCombobox.stories.tsx +1 -1
  184. package/src/web-components/interaction/form/BdsFileInput.stories.tsx +1 -1
  185. package/src/web-components/interaction/form/BdsFormInput.stories.tsx +1 -1
  186. package/src/web-components/interaction/form/BdsNumberInput.stories.tsx +1 -1
  187. package/src/web-components/interaction/form/BdsRadio.stories.tsx +1 -1
  188. package/src/web-components/interaction/form/BdsRadioGroup.stories.tsx +1 -1
  189. package/src/web-components/interaction/form/BdsSegmentedControl.stories.tsx +1 -1
  190. package/src/web-components/interaction/form/BdsSelect.stories.tsx +1 -1
  191. package/src/web-components/interaction/form/BdsSlider.stories.tsx +1 -1
  192. package/src/web-components/interaction/form/BdsSwitch.stories.tsx +1 -1
  193. package/src/web-components/interaction/form/BdsTextarea.stories.tsx +1 -1
  194. package/src/web-components/ui/BdsButtonGroup.stories.tsx +1 -1
  195. package/src/web-components/ui/BdsCard.stories.tsx +1 -1
  196. package/src/web-components/ui/BdsIconWrapper.stories.tsx +1 -1
  197. package/src/web-components/ui/BdsRating.stories.tsx +1 -1
  198. 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
+ }
@@ -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
+ }
@@ -0,0 +1,61 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './Popover.native.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # Popover (React Native)
7
+
8
+ A popover overlay that displays content in a centered modal. The trigger element opens the popover on press; tapping the backdrop dismisses it.
9
+
10
+ > **Status: alpha** — API may change before stable release.
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { Popover } from '@boostdev/components';
16
+
17
+ <Popover
18
+ content={<Text>Additional information here.</Text>}
19
+ accessibilityLabel="More info"
20
+ >
21
+ <Button>Show Info</Button>
22
+ </Popover>
23
+ ```
24
+
25
+ Wrap your tree in `ThemeProvider` (once, at the app root):
26
+
27
+ ```tsx
28
+ import { ThemeProvider } from '@boostdev/components/native/ThemeContext';
29
+
30
+ export default function App() {
31
+ return <ThemeProvider><YourApp /></ThemeProvider>;
32
+ }
33
+ ```
34
+
35
+ ## Examples
36
+
37
+ ### Default
38
+ <Canvas of={Stories.Default} />
39
+
40
+ ### Rich content
41
+ <Canvas of={Stories.RichContent} />
42
+
43
+ ## Props
44
+
45
+ <ArgTypes of={Stories} />
46
+
47
+ ## Props reference
48
+
49
+ | Prop | Type | Default | Description |
50
+ |------|------|---------|-------------|
51
+ | `children` | `ReactNode` | — | Trigger element that opens the popover on press |
52
+ | `content` | `ReactNode` | — | Content displayed inside the popover panel |
53
+ | `accessibilityLabel` | `string` | `'Open popover'` | Label for the trigger and panel |
54
+ | `style` | `StyleProp<ViewStyle>` | — | Override wrapper styles |
55
+
56
+ ## Accessibility
57
+
58
+ - Trigger has `accessibilityRole="button"` with `expanded` state
59
+ - Backdrop press closes the popover
60
+ - Android back button triggers `onRequestClose` to dismiss
61
+ - Panel has shadow elevation for visual depth