@boostdev/design-system-react-native 0.1.0 → 0.2.1

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 (191) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/LICENSE +15 -0
  3. package/README.md +104 -2
  4. package/dist/index.cjs +106 -60
  5. package/dist/index.d.cts +61 -6
  6. package/dist/index.d.ts +61 -6
  7. package/dist/index.js +106 -60
  8. package/package.json +30 -10
  9. package/src/components/interaction/Button/Button.mdx +91 -0
  10. package/src/components/interaction/Button/Button.spec.tsx +55 -0
  11. package/src/components/interaction/Button/Button.stories.tsx +141 -0
  12. package/src/components/interaction/Button/Button.tsx +105 -0
  13. package/src/components/interaction/Dialog/Dialog.mdx +61 -0
  14. package/src/components/interaction/Dialog/Dialog.spec.tsx +73 -0
  15. package/src/components/interaction/Dialog/Dialog.stories.tsx +53 -0
  16. package/src/components/interaction/Dialog/Dialog.tsx +128 -0
  17. package/src/components/interaction/DropdownMenu/DropdownMenu.mdx +74 -0
  18. package/src/components/interaction/DropdownMenu/DropdownMenu.spec.tsx +78 -0
  19. package/src/components/interaction/DropdownMenu/DropdownMenu.stories.tsx +51 -0
  20. package/src/components/interaction/DropdownMenu/DropdownMenu.tsx +254 -0
  21. package/src/components/interaction/Popover/Popover.mdx +61 -0
  22. package/src/components/interaction/Popover/Popover.spec.tsx +73 -0
  23. package/src/components/interaction/Popover/Popover.stories.tsx +44 -0
  24. package/src/components/interaction/Popover/Popover.tsx +87 -0
  25. package/src/components/interaction/Rating/Rating.mdx +55 -0
  26. package/src/components/interaction/Rating/Rating.spec.tsx +38 -0
  27. package/src/components/interaction/Rating/Rating.stories.tsx +37 -0
  28. package/src/components/interaction/Rating/Rating.tsx +50 -0
  29. package/src/components/interaction/SideSheet/SideSheet.mdx +60 -0
  30. package/src/components/interaction/SideSheet/SideSheet.spec.tsx +80 -0
  31. package/src/components/interaction/SideSheet/SideSheet.stories.tsx +33 -0
  32. package/src/components/interaction/SideSheet/SideSheet.tsx +175 -0
  33. package/src/components/interaction/Toast/Toast.mdx +81 -0
  34. package/src/components/interaction/Toast/Toast.spec.tsx +80 -0
  35. package/src/components/interaction/Toast/Toast.stories.tsx +47 -0
  36. package/src/components/interaction/Toast/Toast.tsx +202 -0
  37. package/src/components/interaction/form/Checkbox/Checkbox.mdx +74 -0
  38. package/src/components/interaction/form/Checkbox/Checkbox.spec.tsx +42 -0
  39. package/src/components/interaction/form/Checkbox/Checkbox.stories.tsx +34 -0
  40. package/src/components/interaction/form/Checkbox/Checkbox.tsx +80 -0
  41. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.mdx +78 -0
  42. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.spec.tsx +57 -0
  43. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.stories.tsx +96 -0
  44. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.tsx +61 -0
  45. package/src/components/interaction/form/Combobox/Combobox.mdx +86 -0
  46. package/src/components/interaction/form/Combobox/Combobox.spec.tsx +60 -0
  47. package/src/components/interaction/form/Combobox/Combobox.stories.tsx +99 -0
  48. package/src/components/interaction/form/Combobox/Combobox.tsx +212 -0
  49. package/src/components/interaction/form/FileInput/FileInput.mdx +86 -0
  50. package/src/components/interaction/form/FileInput/FileInput.spec.tsx +73 -0
  51. package/src/components/interaction/form/FileInput/FileInput.stories.tsx +69 -0
  52. package/src/components/interaction/form/FileInput/FileInput.tsx +129 -0
  53. package/src/components/interaction/form/FormInput/FormInput.mdx +81 -0
  54. package/src/components/interaction/form/FormInput/FormInput.spec.tsx +81 -0
  55. package/src/components/interaction/form/FormInput/FormInput.stories.tsx +54 -0
  56. package/src/components/interaction/form/FormInput/FormInput.tsx +123 -0
  57. package/src/components/interaction/form/NumberInput/NumberInput.mdx +87 -0
  58. package/src/components/interaction/form/NumberInput/NumberInput.spec.tsx +63 -0
  59. package/src/components/interaction/form/NumberInput/NumberInput.stories.tsx +63 -0
  60. package/src/components/interaction/form/NumberInput/NumberInput.tsx +199 -0
  61. package/src/components/interaction/form/Radio/Radio.spec.tsx +79 -0
  62. package/src/components/interaction/form/Radio/Radio.stories.tsx +44 -0
  63. package/src/components/interaction/form/Radio/Radio.tsx +79 -0
  64. package/src/components/interaction/form/RadioGroup/RadioGroup.mdx +79 -0
  65. package/src/components/interaction/form/RadioGroup/RadioGroup.spec.tsx +66 -0
  66. package/src/components/interaction/form/RadioGroup/RadioGroup.stories.tsx +96 -0
  67. package/src/components/interaction/form/RadioGroup/RadioGroup.tsx +61 -0
  68. package/src/components/interaction/form/SegmentedControl/SegmentedControl.mdx +78 -0
  69. package/src/components/interaction/form/SegmentedControl/SegmentedControl.spec.tsx +82 -0
  70. package/src/components/interaction/form/SegmentedControl/SegmentedControl.stories.tsx +99 -0
  71. package/src/components/interaction/form/SegmentedControl/SegmentedControl.tsx +121 -0
  72. package/src/components/interaction/form/Select/Select.mdx +85 -0
  73. package/src/components/interaction/form/Select/Select.spec.tsx +64 -0
  74. package/src/components/interaction/form/Select/Select.stories.tsx +75 -0
  75. package/src/components/interaction/form/Select/Select.tsx +252 -0
  76. package/src/components/interaction/form/Slider/Slider.mdx +83 -0
  77. package/src/components/interaction/form/Slider/Slider.spec.tsx +51 -0
  78. package/src/components/interaction/form/Slider/Slider.stories.tsx +58 -0
  79. package/src/components/interaction/form/Slider/Slider.tsx +188 -0
  80. package/src/components/interaction/form/Switch/Switch.spec.tsx +60 -0
  81. package/src/components/interaction/form/Switch/Switch.stories.tsx +33 -0
  82. package/src/components/interaction/form/Switch/Switch.tsx +59 -0
  83. package/src/components/interaction/form/Textarea/Textarea.mdx +76 -0
  84. package/src/components/interaction/form/Textarea/Textarea.spec.tsx +50 -0
  85. package/src/components/interaction/form/Textarea/Textarea.stories.tsx +50 -0
  86. package/src/components/interaction/form/Textarea/Textarea.tsx +108 -0
  87. package/src/components/interaction/form/atoms/Label.spec.tsx +39 -0
  88. package/src/components/interaction/form/atoms/Label.stories.tsx +18 -0
  89. package/src/components/interaction/form/atoms/Label.tsx +29 -0
  90. package/src/components/interaction/form/atoms/Message.spec.tsx +30 -0
  91. package/src/components/interaction/form/atoms/Message.stories.tsx +22 -0
  92. package/src/components/interaction/form/atoms/Message.tsx +34 -0
  93. package/src/components/layout/ButtonGroup/ButtonGroup.spec.tsx +83 -0
  94. package/src/components/layout/ButtonGroup/ButtonGroup.stories.tsx +44 -0
  95. package/src/components/layout/ButtonGroup/ButtonGroup.tsx +44 -0
  96. package/src/components/layout/Card/Card.spec.tsx +102 -0
  97. package/src/components/layout/Card/Card.stories.tsx +51 -0
  98. package/src/components/layout/Card/Card.tsx +89 -0
  99. package/src/components/layout/IconWrapper/IconWrapper.spec.tsx +39 -0
  100. package/src/components/layout/IconWrapper/IconWrapper.stories.tsx +37 -0
  101. package/src/components/layout/IconWrapper/IconWrapper.tsx +20 -0
  102. package/src/components/layout/SectionHeader/SectionHeader.spec.tsx +51 -0
  103. package/src/components/layout/SectionHeader/SectionHeader.stories.tsx +36 -0
  104. package/src/components/layout/SectionHeader/SectionHeader.tsx +79 -0
  105. package/src/components/ui/Accordion/Accordion.mdx +65 -0
  106. package/src/components/ui/Accordion/Accordion.spec.tsx +69 -0
  107. package/src/components/ui/Accordion/Accordion.stories.tsx +52 -0
  108. package/src/components/ui/Accordion/Accordion.tsx +141 -0
  109. package/src/components/ui/Alert/Alert.mdx +62 -0
  110. package/src/components/ui/Alert/Alert.spec.tsx +76 -0
  111. package/src/components/ui/Alert/Alert.stories.tsx +41 -0
  112. package/src/components/ui/Alert/Alert.tsx +94 -0
  113. package/src/components/ui/Avatar/Avatar.mdx +50 -0
  114. package/src/components/ui/Avatar/Avatar.spec.tsx +47 -0
  115. package/src/components/ui/Avatar/Avatar.stories.tsx +32 -0
  116. package/src/components/ui/Avatar/Avatar.tsx +85 -0
  117. package/src/components/ui/Badge/Badge.mdx +54 -0
  118. package/src/components/ui/Badge/Badge.spec.tsx +45 -0
  119. package/src/components/ui/Badge/Badge.stories.tsx +34 -0
  120. package/src/components/ui/Badge/Badge.tsx +50 -0
  121. package/src/components/ui/Breadcrumb/Breadcrumb.mdx +52 -0
  122. package/src/components/ui/Breadcrumb/Breadcrumb.spec.tsx +49 -0
  123. package/src/components/ui/Breadcrumb/Breadcrumb.stories.tsx +46 -0
  124. package/src/components/ui/Breadcrumb/Breadcrumb.tsx +83 -0
  125. package/src/components/ui/Calendar/Calendar.mdx +58 -0
  126. package/src/components/ui/Calendar/Calendar.spec.tsx +92 -0
  127. package/src/components/ui/Calendar/Calendar.stories.tsx +45 -0
  128. package/src/components/ui/Calendar/Calendar.tsx +294 -0
  129. package/src/components/ui/Carousel/Carousel.mdx +55 -0
  130. package/src/components/ui/Carousel/Carousel.spec.tsx +55 -0
  131. package/src/components/ui/Carousel/Carousel.stories.tsx +59 -0
  132. package/src/components/ui/Carousel/Carousel.tsx +184 -0
  133. package/src/components/ui/Collapsible/Collapsible.mdx +53 -0
  134. package/src/components/ui/Collapsible/Collapsible.spec.tsx +68 -0
  135. package/src/components/ui/Collapsible/Collapsible.stories.tsx +48 -0
  136. package/src/components/ui/Collapsible/Collapsible.tsx +139 -0
  137. package/src/components/ui/DescriptionList/DescriptionList.mdx +50 -0
  138. package/src/components/ui/DescriptionList/DescriptionList.spec.tsx +37 -0
  139. package/src/components/ui/DescriptionList/DescriptionList.stories.tsx +48 -0
  140. package/src/components/ui/DescriptionList/DescriptionList.tsx +54 -0
  141. package/src/components/ui/Link/Link.mdx +53 -0
  142. package/src/components/ui/Link/Link.spec.tsx +35 -0
  143. package/src/components/ui/Link/Link.stories.tsx +46 -0
  144. package/src/components/ui/Link/Link.tsx +48 -0
  145. package/src/components/ui/Loading/Loading.spec.tsx +24 -0
  146. package/src/components/ui/Loading/Loading.stories.tsx +31 -0
  147. package/src/components/ui/Loading/Loading.tsx +29 -0
  148. package/src/components/ui/NotificationBanner/NotificationBanner.spec.tsx +85 -0
  149. package/src/components/ui/NotificationBanner/NotificationBanner.stories.tsx +37 -0
  150. package/src/components/ui/NotificationBanner/NotificationBanner.tsx +76 -0
  151. package/src/components/ui/Pagination/Pagination.mdx +62 -0
  152. package/src/components/ui/Pagination/Pagination.spec.tsx +86 -0
  153. package/src/components/ui/Pagination/Pagination.stories.tsx +47 -0
  154. package/src/components/ui/Pagination/Pagination.tsx +157 -0
  155. package/src/components/ui/Progress/Progress.spec.tsx +56 -0
  156. package/src/components/ui/Progress/Progress.stories.tsx +34 -0
  157. package/src/components/ui/Progress/Progress.tsx +84 -0
  158. package/src/components/ui/ProgressCircle/ProgressCircle.mdx +63 -0
  159. package/src/components/ui/ProgressCircle/ProgressCircle.spec.tsx +39 -0
  160. package/src/components/ui/ProgressCircle/ProgressCircle.stories.tsx +36 -0
  161. package/src/components/ui/ProgressCircle/ProgressCircle.tsx +176 -0
  162. package/src/components/ui/Separator/Separator.spec.tsx +39 -0
  163. package/src/components/ui/Separator/Separator.stories.tsx +38 -0
  164. package/src/components/ui/Separator/Separator.tsx +32 -0
  165. package/src/components/ui/Skeleton/Skeleton.spec.tsx +39 -0
  166. package/src/components/ui/Skeleton/Skeleton.stories.tsx +31 -0
  167. package/src/components/ui/Skeleton/Skeleton.tsx +42 -0
  168. package/src/components/ui/Table/Table.mdx +72 -0
  169. package/src/components/ui/Table/Table.spec.tsx +83 -0
  170. package/src/components/ui/Table/Table.stories.tsx +55 -0
  171. package/src/components/ui/Table/Table.tsx +196 -0
  172. package/src/components/ui/Tabs/Tabs.mdx +65 -0
  173. package/src/components/ui/Tabs/Tabs.spec.tsx +65 -0
  174. package/src/components/ui/Tabs/Tabs.stories.tsx +57 -0
  175. package/src/components/ui/Tabs/Tabs.tsx +133 -0
  176. package/src/components/ui/Tooltip/Tooltip.mdx +47 -0
  177. package/src/components/ui/Tooltip/Tooltip.spec.tsx +37 -0
  178. package/src/components/ui/Tooltip/Tooltip.stories.tsx +38 -0
  179. package/src/components/ui/Tooltip/Tooltip.tsx +57 -0
  180. package/src/components/ui/Typography/Typography.mdx +56 -0
  181. package/src/components/ui/Typography/Typography.spec.tsx +49 -0
  182. package/src/components/ui/Typography/Typography.stories.tsx +36 -0
  183. package/src/components/ui/Typography/Typography.tsx +65 -0
  184. package/src/index.ts +86 -0
  185. package/src/native/ReactNavigationTheme.mdx +140 -0
  186. package/src/native/ReactNavigationTheme.spec.tsx +35 -0
  187. package/src/native/ReactNavigationTheme.ts +102 -0
  188. package/src/native/ThemeContext.spec.tsx +57 -0
  189. package/src/native/ThemeContext.tsx +48 -0
  190. package/src/native/tokens.ts +13 -0
  191. package/src/test/setup.ts +1 -0
@@ -0,0 +1,73 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { Text, Pressable } from 'react-native';
3
+ import { ThemeProvider } from '../../../native/ThemeContext';
4
+ import { Popover } from './Popover';
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
+ describe('Popover (native)', () => {
15
+ it('renders the trigger element', () => {
16
+ renderWithTheme(
17
+ <Popover content={<Text>Popover content</Text>}>
18
+ <Trigger>Trigger</Trigger>
19
+ </Popover>,
20
+ );
21
+ expect(screen.getByText('Trigger')).toBeInTheDocument();
22
+ });
23
+
24
+ it('does not show content initially', () => {
25
+ renderWithTheme(
26
+ <Popover content={<Text>Hidden content</Text>}>
27
+ <Trigger>Trigger</Trigger>
28
+ </Popover>,
29
+ );
30
+ expect(screen.queryByText('Hidden content')).not.toBeInTheDocument();
31
+ });
32
+
33
+ it('shows content when trigger is pressed', () => {
34
+ renderWithTheme(
35
+ <Popover content={<Text>Visible content</Text>} accessibilityLabel="Info">
36
+ <Trigger>Trigger</Trigger>
37
+ </Popover>,
38
+ );
39
+ fireEvent.click(screen.getByRole('button', { name: 'Info' }));
40
+ expect(screen.getByText('Visible content')).toBeInTheDocument();
41
+ });
42
+
43
+ it('renders trigger with button role and custom accessibility label', () => {
44
+ renderWithTheme(
45
+ <Popover content={<Text>Content</Text>} accessibilityLabel="Details">
46
+ <Trigger>Trigger</Trigger>
47
+ </Popover>,
48
+ );
49
+ const trigger = screen.getByRole('button', { name: 'Details' });
50
+ expect(trigger).toBeInTheDocument();
51
+ });
52
+
53
+ it('renders backdrop with close label when open', () => {
54
+ renderWithTheme(
55
+ <Popover content={<Text>Closable</Text>} accessibilityLabel="Test">
56
+ <Trigger>Trigger</Trigger>
57
+ </Popover>,
58
+ );
59
+ fireEvent.click(screen.getByRole('button', { name: 'Test' }));
60
+ expect(screen.getByText('Closable')).toBeInTheDocument();
61
+ // Verify the close backdrop button exists
62
+ expect(screen.getByRole('button', { name: 'Close popover' })).toBeInTheDocument();
63
+ });
64
+
65
+ it('uses default accessibility label when none provided', () => {
66
+ renderWithTheme(
67
+ <Popover content={<Text>Content</Text>}>
68
+ <Trigger>Trigger</Trigger>
69
+ </Popover>,
70
+ );
71
+ expect(screen.getByRole('button', { name: 'Open popover' })).toBeInTheDocument();
72
+ });
73
+ });
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+ import { Text, View } from 'react-native';
3
+ import type { Meta, StoryObj } from '@storybook/react';
4
+ import { ThemeProvider } from '../../../native/ThemeContext';
5
+ import { Popover } from './Popover';
6
+ import { Button } from '../Button/Button';
7
+
8
+ const meta = {
9
+ tags: ['!stable', 'alpha'],
10
+ title: 'React Native/Interaction/Popover',
11
+ component: Popover,
12
+ decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
13
+ parameters: { layout: 'centered' },
14
+ } satisfies Meta<typeof Popover>;
15
+
16
+ export default meta;
17
+ type Story = StoryObj<typeof meta>;
18
+
19
+ export const Default: Story = {
20
+ render: () => (
21
+ <Popover
22
+ content={<Text>This is popover content displayed in a modal overlay.</Text>}
23
+ accessibilityLabel="More info"
24
+ >
25
+ <Button>Show Popover</Button>
26
+ </Popover>
27
+ ),
28
+ };
29
+
30
+ export const RichContent: Story = {
31
+ render: () => (
32
+ <Popover
33
+ content={
34
+ <View style={{ gap: 8 }}>
35
+ <Text style={{ fontWeight: '600', fontSize: 16 }}>Help</Text>
36
+ <Text>Press the button below to submit your response. You can edit your answer before submitting.</Text>
37
+ </View>
38
+ }
39
+ accessibilityLabel="Help information"
40
+ >
41
+ <Button variant="outline">Help</Button>
42
+ </Popover>
43
+ ),
44
+ };
@@ -0,0 +1,87 @@
1
+ import React, { ReactElement, ReactNode, useState } from 'react';
2
+ import {
3
+ Modal,
4
+ View,
5
+ Pressable,
6
+ StyleSheet,
7
+ StyleProp,
8
+ ViewStyle,
9
+ } from 'react-native';
10
+ import { spacing, border } from '../../../native/tokens';
11
+ import { useTheme } from '../../../native/ThemeContext';
12
+
13
+ interface PopoverProps {
14
+ children: ReactElement;
15
+ content: ReactNode;
16
+ accessibilityLabel?: string;
17
+ style?: StyleProp<ViewStyle>;
18
+ }
19
+
20
+ const styles = StyleSheet.create({
21
+ wrapper: {
22
+ alignSelf: 'flex-start',
23
+ },
24
+ overlay: {
25
+ flex: 1,
26
+ justifyContent: 'center',
27
+ alignItems: 'center',
28
+ backgroundColor: 'rgba(0, 0, 0, 0.25)',
29
+ padding: spacing.m,
30
+ },
31
+ panel: {
32
+ width: '100%',
33
+ maxWidth: 360,
34
+ borderRadius: border.radius.s,
35
+ padding: spacing.m,
36
+ shadowColor: '#000',
37
+ shadowOffset: { width: 0, height: 2 },
38
+ shadowOpacity: 0.15,
39
+ shadowRadius: 8,
40
+ elevation: 4,
41
+ },
42
+ });
43
+
44
+ export function Popover({
45
+ children,
46
+ content,
47
+ accessibilityLabel,
48
+ style,
49
+ }: Readonly<PopoverProps>) {
50
+ const { colors } = useTheme();
51
+ const [isOpen, setIsOpen] = useState(false);
52
+
53
+ return (
54
+ <View style={[styles.wrapper, style]}>
55
+ {React.cloneElement(children as ReactElement<Record<string, unknown>>, {
56
+ onPress: () => setIsOpen(true),
57
+ accessibilityRole: 'button',
58
+ accessibilityLabel: accessibilityLabel ?? 'Open popover',
59
+ })}
60
+
61
+ <Modal
62
+ visible={isOpen}
63
+ transparent
64
+ animationType="fade"
65
+ onRequestClose={() => setIsOpen(false)}
66
+ >
67
+ <Pressable
68
+ style={styles.overlay}
69
+ onPress={() => setIsOpen(false)}
70
+ accessibilityRole="button"
71
+ accessibilityLabel="Close popover"
72
+ >
73
+ <Pressable
74
+ style={[styles.panel, { backgroundColor: colors.colorBg }]}
75
+ onPress={() => {
76
+ /* prevent close when pressing panel */
77
+ }}
78
+ accessibilityRole="none"
79
+ accessibilityLabel={accessibilityLabel}
80
+ >
81
+ {content}
82
+ </Pressable>
83
+ </Pressable>
84
+ </Modal>
85
+ </View>
86
+ );
87
+ }
@@ -0,0 +1,55 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './Rating.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # Rating (React Native)
7
+
8
+ React Native read-only star rating display using text characters. Shows filled and empty stars based on the `value` prop.
9
+
10
+ > **Status: alpha** — API may change before stable release.
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { Rating } from '@boostdev/design-system-components';
16
+
17
+ <Rating value={4} />
18
+ <Rating value={7} max={10} />
19
+ ```
20
+
21
+ ## Examples
22
+
23
+ ### Default (3 of 5)
24
+ <Canvas of={Stories.Default} />
25
+
26
+ ### Full rating
27
+ <Canvas of={Stories.Full} />
28
+
29
+ ### Empty rating
30
+ <Canvas of={Stories.Empty} />
31
+
32
+ ### Custom max
33
+ <Canvas of={Stories.CustomMax} />
34
+
35
+ ### All variants
36
+ <Canvas of={Stories.AllVariants} />
37
+
38
+ ## Props
39
+
40
+ <ArgTypes of={Stories} />
41
+
42
+ ## Props reference
43
+
44
+ | Prop | Type | Default | Description |
45
+ |------|------|---------|-------------|
46
+ | `value` | `number` | — | Number of filled stars |
47
+ | `max` | `number` | `5` | Total number of stars |
48
+ | `accessibilityLabel` | `string` | `"{value} out of {max} stars"` | Screen reader label |
49
+ | `style` | `StyleProp<ViewStyle>` | — | Override container styles |
50
+
51
+ ## Accessibility
52
+
53
+ - Uses `accessibilityRole="image"` since the rating is a visual representation
54
+ - Auto-generates an accessibility label like "3 out of 5 stars" when not provided
55
+ - Individual stars are hidden from assistive technologies
@@ -0,0 +1,38 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { ThemeProvider } from '../../../native/ThemeContext';
3
+ import { Rating } from './Rating';
4
+
5
+ function renderWithTheme(ui: React.ReactElement) {
6
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
7
+ }
8
+
9
+ describe('Rating (native)', () => {
10
+ it('has image role with auto-generated accessible label', () => {
11
+ renderWithTheme(<Rating value={3} />);
12
+ expect(screen.getByRole('img', { name: '3 out of 5 stars' })).toBeInTheDocument();
13
+ });
14
+
15
+ it('renders the correct number of filled stars', () => {
16
+ renderWithTheme(<Rating value={3} />);
17
+ const stars = screen.getAllByText('\u2605');
18
+ expect(stars).toHaveLength(5);
19
+ });
20
+
21
+ it('renders all stars as filled characters', () => {
22
+ renderWithTheme(<Rating value={2} />);
23
+ const stars = screen.getAllByText('\u2605');
24
+ expect(stars).toHaveLength(5);
25
+ });
26
+
27
+ it('uses custom max value', () => {
28
+ renderWithTheme(<Rating value={7} max={10} />);
29
+ expect(screen.getByRole('img', { name: '7 out of 10 stars' })).toBeInTheDocument();
30
+ const stars = screen.getAllByText('\u2605');
31
+ expect(stars).toHaveLength(10);
32
+ });
33
+
34
+ it('uses custom accessibilityLabel when provided', () => {
35
+ renderWithTheme(<Rating value={4} accessibilityLabel="Excellent rating" />);
36
+ expect(screen.getByRole('img', { name: 'Excellent rating' })).toBeInTheDocument();
37
+ });
38
+ });
@@ -0,0 +1,37 @@
1
+ import React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { ThemeProvider } from '../../../native/ThemeContext';
4
+ import { Rating } from './Rating';
5
+
6
+ const meta = {
7
+ tags: ['!stable', 'alpha'],
8
+ title: 'React Native/Interaction/Rating',
9
+ component: Rating,
10
+ decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
11
+ parameters: { layout: 'centered' },
12
+ argTypes: {
13
+ value: { control: { type: 'range', min: 0, max: 5, step: 1 } },
14
+ max: { control: { type: 'number', min: 1, max: 10 } },
15
+ },
16
+ } satisfies Meta<typeof Rating>;
17
+
18
+ export default meta;
19
+ type Story = StoryObj<typeof meta>;
20
+
21
+ export const Default: Story = { args: { value: 3 } };
22
+ export const Full: Story = { args: { value: 5 } };
23
+ export const Empty: Story = { args: { value: 0 } };
24
+ export const CustomMax: Story = { args: { value: 7, max: 10 } };
25
+ export const WithLabel: Story = {
26
+ args: { value: 4, accessibilityLabel: 'Customer rating: 4 out of 5' },
27
+ };
28
+
29
+ export const AllVariants: Story = {
30
+ render: () => (
31
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
32
+ {[0, 1, 2, 3, 4, 5].map(v => (
33
+ <Rating key={v} value={v} />
34
+ ))}
35
+ </div>
36
+ ),
37
+ };
@@ -0,0 +1,50 @@
1
+ import { StyleSheet, View, Text, StyleProp, ViewStyle } from 'react-native';
2
+ import { font } from '../../../native/tokens';
3
+ import { useTheme } from '../../../native/ThemeContext';
4
+
5
+ interface RatingProps {
6
+ value: number;
7
+ max?: number;
8
+ style?: StyleProp<ViewStyle>;
9
+ accessibilityLabel?: string;
10
+ }
11
+
12
+ const STAR_SIZE = 24;
13
+
14
+ const styles = StyleSheet.create({
15
+ container: {
16
+ flexDirection: 'row',
17
+ alignItems: 'center',
18
+ },
19
+ star: {
20
+ fontSize: STAR_SIZE,
21
+ fontFamily: font.family.body,
22
+ },
23
+ });
24
+
25
+ export function Rating({ value, max = 5, style, accessibilityLabel }: Readonly<RatingProps>) {
26
+ const { colors } = useTheme();
27
+ const label = accessibilityLabel ?? `${value} out of ${max} stars`;
28
+
29
+ return (
30
+ <View
31
+ style={[styles.container, style]}
32
+ accessibilityRole="image"
33
+ accessibilityLabel={label}
34
+ >
35
+ {Array.from({ length: max }).map((_, i) => (
36
+ <Text
37
+ key={i}
38
+ style={[
39
+ styles.star,
40
+ { color: i < value ? colors.colorWarning : colors.colorBgSubtle },
41
+ ]}
42
+ importantForAccessibility="no"
43
+ accessibilityElementsHidden={true}
44
+ >
45
+ {'\u2605'}
46
+ </Text>
47
+ ))}
48
+ </View>
49
+ );
50
+ }
@@ -0,0 +1,60 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './SideSheet.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # SideSheet (React Native)
7
+
8
+ A sliding panel that slides in from the bottom of the screen. Built on React Native `Modal` with `Animated` for smooth slide and backdrop fade transitions.
9
+
10
+ > **Naming note:** previously called `Drawer`. Renamed to `SideSheet` to avoid collision with `@react-navigation/drawer`, which is a top-level *navigator*, not a UI primitive. This component is a transient side sheet for content (filters, details, forms) — it has nothing to do with screen routing.
11
+
12
+ > **Status: alpha** — API may change before stable release.
13
+
14
+ ## Usage
15
+
16
+ ```tsx
17
+ import { SideSheet } from '@boostdev/design-system-react-native';
18
+
19
+ const [open, setOpen] = useState(false);
20
+
21
+ <Button onPress={() => setOpen(true)}>Open</Button>
22
+ <SideSheet isOpen={open} onClose={() => setOpen(false)} title="Settings">
23
+ <Text>Side sheet content here.</Text>
24
+ </SideSheet>
25
+ ```
26
+
27
+ Wrap your tree in `ThemeProvider` (once, at the app root):
28
+
29
+ ```tsx
30
+ import { ThemeProvider } from '@boostdev/design-system-react-native';
31
+
32
+ export default function App() {
33
+ return <ThemeProvider><YourApp /></ThemeProvider>;
34
+ }
35
+ ```
36
+
37
+ ## Examples
38
+
39
+ <Canvas of={Stories.Default} />
40
+
41
+ ## Props
42
+
43
+ <ArgTypes of={Stories} />
44
+
45
+ ## Props reference
46
+
47
+ | Prop | Type | Default | Description |
48
+ |------|------|---------|-------------|
49
+ | `isOpen` | `boolean` | — | Controls visibility of the side sheet |
50
+ | `onClose` | `() => void` | — | Called when backdrop or close button is pressed |
51
+ | `title` | `ReactNode` | — | Title displayed in the side sheet header |
52
+ | `children` | `ReactNode` | — | Side sheet body content |
53
+ | `style` | `StyleProp<ViewStyle>` | — | Override panel styles |
54
+
55
+ ## Accessibility
56
+
57
+ - Backdrop press and close button both call `onClose`
58
+ - Close button has `accessibilityLabel="Close side sheet"`
59
+ - Android back button triggers `onRequestClose` which calls `onClose`
60
+ - Backdrop fades in, panel slides from bottom
@@ -0,0 +1,80 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { ThemeProvider } from '../../../native/ThemeContext';
3
+ import { SideSheet } from './SideSheet';
4
+
5
+ function renderWithTheme(ui: React.ReactElement) {
6
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
7
+ }
8
+
9
+ describe('SideSheet (native)', () => {
10
+ it('renders children when open', () => {
11
+ renderWithTheme(
12
+ <SideSheet isOpen onClose={() => {}}>
13
+ <span>SideSheet content</span>
14
+ </SideSheet>,
15
+ );
16
+ expect(screen.getByText('SideSheet content')).toBeInTheDocument();
17
+ });
18
+
19
+ it('does not render children when closed', () => {
20
+ renderWithTheme(
21
+ <SideSheet isOpen={false} onClose={() => {}}>
22
+ <span>Hidden side sheet</span>
23
+ </SideSheet>,
24
+ );
25
+ expect(screen.queryByText('Hidden side sheet')).not.toBeInTheDocument();
26
+ });
27
+
28
+ it('renders the title when provided', () => {
29
+ renderWithTheme(
30
+ <SideSheet isOpen onClose={() => {}} title="Settings">
31
+ <span>Body</span>
32
+ </SideSheet>,
33
+ );
34
+ expect(screen.getByText('Settings')).toBeInTheDocument();
35
+ });
36
+
37
+ it('renders a close button with accessible label', () => {
38
+ renderWithTheme(
39
+ <SideSheet isOpen onClose={() => {}}>
40
+ <span>Body</span>
41
+ </SideSheet>,
42
+ );
43
+ const closeButtons = screen.getAllByRole('button', { name: 'Close side sheet' });
44
+ expect(closeButtons.length).toBeGreaterThanOrEqual(1);
45
+ });
46
+
47
+ it('calls onClose when close button is pressed', () => {
48
+ const onClose = vi.fn();
49
+ renderWithTheme(
50
+ <SideSheet isOpen onClose={onClose} title="Test">
51
+ <span>Body</span>
52
+ </SideSheet>,
53
+ );
54
+ const closeButtons = screen.getAllByRole('button', { name: 'Close side sheet' });
55
+ const innerClose = closeButtons[closeButtons.length - 1];
56
+ fireEvent.click(innerClose);
57
+ expect(onClose).toHaveBeenCalledTimes(1);
58
+ });
59
+
60
+ it('calls onClose when backdrop is pressed', () => {
61
+ const onClose = vi.fn();
62
+ renderWithTheme(
63
+ <SideSheet isOpen onClose={onClose}>
64
+ <span>Body</span>
65
+ </SideSheet>,
66
+ );
67
+ const closeButtons = screen.getAllByRole('button', { name: 'Close side sheet' });
68
+ fireEvent.click(closeButtons[0]);
69
+ expect(onClose).toHaveBeenCalledTimes(1);
70
+ });
71
+
72
+ it('renders panel when open', () => {
73
+ const { container } = renderWithTheme(
74
+ <SideSheet isOpen onClose={() => {}}>
75
+ <span>Body</span>
76
+ </SideSheet>,
77
+ );
78
+ expect(container).toBeInTheDocument();
79
+ });
80
+ });
@@ -0,0 +1,33 @@
1
+ import React, { useState } from 'react';
2
+ import { Text } from 'react-native';
3
+ import type { Meta, StoryObj } from '@storybook/react';
4
+ import { ThemeProvider } from '../../../native/ThemeContext';
5
+ import { SideSheet } from './SideSheet';
6
+ import { Button } from '../Button/Button';
7
+
8
+ const meta = {
9
+ tags: ['!stable', 'alpha'],
10
+ title: 'React Native/Interaction/SideSheet',
11
+ component: SideSheet,
12
+ decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
13
+ parameters: { layout: 'centered' },
14
+ } satisfies Meta<typeof SideSheet>;
15
+
16
+ export default meta;
17
+ type Story = StoryObj<typeof meta>;
18
+
19
+ function SideSheetDemo() {
20
+ const [open, setOpen] = useState(false);
21
+ return (
22
+ <>
23
+ <Button onPress={() => setOpen(true)}>Open side sheet</Button>
24
+ <SideSheet isOpen={open} onClose={() => setOpen(false)} title="Settings">
25
+ <Text>Side sheet content slides in from the bottom.</Text>
26
+ </SideSheet>
27
+ </>
28
+ );
29
+ }
30
+
31
+ export const Default: Story = {
32
+ render: () => <SideSheetDemo />,
33
+ };