@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.
- package/CHANGELOG.md +55 -0
- package/LICENSE +15 -0
- package/README.md +104 -2
- package/dist/index.cjs +106 -60
- package/dist/index.d.cts +61 -6
- package/dist/index.d.ts +61 -6
- package/dist/index.js +106 -60
- package/package.json +30 -10
- package/src/components/interaction/Button/Button.mdx +91 -0
- package/src/components/interaction/Button/Button.spec.tsx +55 -0
- package/src/components/interaction/Button/Button.stories.tsx +141 -0
- package/src/components/interaction/Button/Button.tsx +105 -0
- package/src/components/interaction/Dialog/Dialog.mdx +61 -0
- package/src/components/interaction/Dialog/Dialog.spec.tsx +73 -0
- package/src/components/interaction/Dialog/Dialog.stories.tsx +53 -0
- package/src/components/interaction/Dialog/Dialog.tsx +128 -0
- package/src/components/interaction/DropdownMenu/DropdownMenu.mdx +74 -0
- package/src/components/interaction/DropdownMenu/DropdownMenu.spec.tsx +78 -0
- package/src/components/interaction/DropdownMenu/DropdownMenu.stories.tsx +51 -0
- package/src/components/interaction/DropdownMenu/DropdownMenu.tsx +254 -0
- package/src/components/interaction/Popover/Popover.mdx +61 -0
- package/src/components/interaction/Popover/Popover.spec.tsx +73 -0
- package/src/components/interaction/Popover/Popover.stories.tsx +44 -0
- package/src/components/interaction/Popover/Popover.tsx +87 -0
- package/src/components/interaction/Rating/Rating.mdx +55 -0
- package/src/components/interaction/Rating/Rating.spec.tsx +38 -0
- package/src/components/interaction/Rating/Rating.stories.tsx +37 -0
- package/src/components/interaction/Rating/Rating.tsx +50 -0
- package/src/components/interaction/SideSheet/SideSheet.mdx +60 -0
- package/src/components/interaction/SideSheet/SideSheet.spec.tsx +80 -0
- package/src/components/interaction/SideSheet/SideSheet.stories.tsx +33 -0
- package/src/components/interaction/SideSheet/SideSheet.tsx +175 -0
- package/src/components/interaction/Toast/Toast.mdx +81 -0
- package/src/components/interaction/Toast/Toast.spec.tsx +80 -0
- package/src/components/interaction/Toast/Toast.stories.tsx +47 -0
- package/src/components/interaction/Toast/Toast.tsx +202 -0
- package/src/components/interaction/form/Checkbox/Checkbox.mdx +74 -0
- package/src/components/interaction/form/Checkbox/Checkbox.spec.tsx +42 -0
- package/src/components/interaction/form/Checkbox/Checkbox.stories.tsx +34 -0
- package/src/components/interaction/form/Checkbox/Checkbox.tsx +80 -0
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.mdx +78 -0
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.spec.tsx +57 -0
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.stories.tsx +96 -0
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.tsx +61 -0
- package/src/components/interaction/form/Combobox/Combobox.mdx +86 -0
- package/src/components/interaction/form/Combobox/Combobox.spec.tsx +60 -0
- package/src/components/interaction/form/Combobox/Combobox.stories.tsx +99 -0
- package/src/components/interaction/form/Combobox/Combobox.tsx +212 -0
- package/src/components/interaction/form/FileInput/FileInput.mdx +86 -0
- package/src/components/interaction/form/FileInput/FileInput.spec.tsx +73 -0
- package/src/components/interaction/form/FileInput/FileInput.stories.tsx +69 -0
- package/src/components/interaction/form/FileInput/FileInput.tsx +129 -0
- package/src/components/interaction/form/FormInput/FormInput.mdx +81 -0
- package/src/components/interaction/form/FormInput/FormInput.spec.tsx +81 -0
- package/src/components/interaction/form/FormInput/FormInput.stories.tsx +54 -0
- package/src/components/interaction/form/FormInput/FormInput.tsx +123 -0
- package/src/components/interaction/form/NumberInput/NumberInput.mdx +87 -0
- package/src/components/interaction/form/NumberInput/NumberInput.spec.tsx +63 -0
- package/src/components/interaction/form/NumberInput/NumberInput.stories.tsx +63 -0
- package/src/components/interaction/form/NumberInput/NumberInput.tsx +199 -0
- package/src/components/interaction/form/Radio/Radio.spec.tsx +79 -0
- package/src/components/interaction/form/Radio/Radio.stories.tsx +44 -0
- package/src/components/interaction/form/Radio/Radio.tsx +79 -0
- package/src/components/interaction/form/RadioGroup/RadioGroup.mdx +79 -0
- package/src/components/interaction/form/RadioGroup/RadioGroup.spec.tsx +66 -0
- package/src/components/interaction/form/RadioGroup/RadioGroup.stories.tsx +96 -0
- package/src/components/interaction/form/RadioGroup/RadioGroup.tsx +61 -0
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.mdx +78 -0
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.spec.tsx +82 -0
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.stories.tsx +99 -0
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.tsx +121 -0
- package/src/components/interaction/form/Select/Select.mdx +85 -0
- package/src/components/interaction/form/Select/Select.spec.tsx +64 -0
- package/src/components/interaction/form/Select/Select.stories.tsx +75 -0
- package/src/components/interaction/form/Select/Select.tsx +252 -0
- package/src/components/interaction/form/Slider/Slider.mdx +83 -0
- package/src/components/interaction/form/Slider/Slider.spec.tsx +51 -0
- package/src/components/interaction/form/Slider/Slider.stories.tsx +58 -0
- package/src/components/interaction/form/Slider/Slider.tsx +188 -0
- package/src/components/interaction/form/Switch/Switch.spec.tsx +60 -0
- package/src/components/interaction/form/Switch/Switch.stories.tsx +33 -0
- package/src/components/interaction/form/Switch/Switch.tsx +59 -0
- package/src/components/interaction/form/Textarea/Textarea.mdx +76 -0
- package/src/components/interaction/form/Textarea/Textarea.spec.tsx +50 -0
- package/src/components/interaction/form/Textarea/Textarea.stories.tsx +50 -0
- package/src/components/interaction/form/Textarea/Textarea.tsx +108 -0
- package/src/components/interaction/form/atoms/Label.spec.tsx +39 -0
- package/src/components/interaction/form/atoms/Label.stories.tsx +18 -0
- package/src/components/interaction/form/atoms/Label.tsx +29 -0
- package/src/components/interaction/form/atoms/Message.spec.tsx +30 -0
- package/src/components/interaction/form/atoms/Message.stories.tsx +22 -0
- package/src/components/interaction/form/atoms/Message.tsx +34 -0
- package/src/components/layout/ButtonGroup/ButtonGroup.spec.tsx +83 -0
- package/src/components/layout/ButtonGroup/ButtonGroup.stories.tsx +44 -0
- package/src/components/layout/ButtonGroup/ButtonGroup.tsx +44 -0
- package/src/components/layout/Card/Card.spec.tsx +102 -0
- package/src/components/layout/Card/Card.stories.tsx +51 -0
- package/src/components/layout/Card/Card.tsx +89 -0
- package/src/components/layout/IconWrapper/IconWrapper.spec.tsx +39 -0
- package/src/components/layout/IconWrapper/IconWrapper.stories.tsx +37 -0
- package/src/components/layout/IconWrapper/IconWrapper.tsx +20 -0
- package/src/components/layout/SectionHeader/SectionHeader.spec.tsx +51 -0
- package/src/components/layout/SectionHeader/SectionHeader.stories.tsx +36 -0
- package/src/components/layout/SectionHeader/SectionHeader.tsx +79 -0
- package/src/components/ui/Accordion/Accordion.mdx +65 -0
- package/src/components/ui/Accordion/Accordion.spec.tsx +69 -0
- package/src/components/ui/Accordion/Accordion.stories.tsx +52 -0
- package/src/components/ui/Accordion/Accordion.tsx +141 -0
- package/src/components/ui/Alert/Alert.mdx +62 -0
- package/src/components/ui/Alert/Alert.spec.tsx +76 -0
- package/src/components/ui/Alert/Alert.stories.tsx +41 -0
- package/src/components/ui/Alert/Alert.tsx +94 -0
- package/src/components/ui/Avatar/Avatar.mdx +50 -0
- package/src/components/ui/Avatar/Avatar.spec.tsx +47 -0
- package/src/components/ui/Avatar/Avatar.stories.tsx +32 -0
- package/src/components/ui/Avatar/Avatar.tsx +85 -0
- package/src/components/ui/Badge/Badge.mdx +54 -0
- package/src/components/ui/Badge/Badge.spec.tsx +45 -0
- package/src/components/ui/Badge/Badge.stories.tsx +34 -0
- package/src/components/ui/Badge/Badge.tsx +50 -0
- package/src/components/ui/Breadcrumb/Breadcrumb.mdx +52 -0
- package/src/components/ui/Breadcrumb/Breadcrumb.spec.tsx +49 -0
- package/src/components/ui/Breadcrumb/Breadcrumb.stories.tsx +46 -0
- package/src/components/ui/Breadcrumb/Breadcrumb.tsx +83 -0
- package/src/components/ui/Calendar/Calendar.mdx +58 -0
- package/src/components/ui/Calendar/Calendar.spec.tsx +92 -0
- package/src/components/ui/Calendar/Calendar.stories.tsx +45 -0
- package/src/components/ui/Calendar/Calendar.tsx +294 -0
- package/src/components/ui/Carousel/Carousel.mdx +55 -0
- package/src/components/ui/Carousel/Carousel.spec.tsx +55 -0
- package/src/components/ui/Carousel/Carousel.stories.tsx +59 -0
- package/src/components/ui/Carousel/Carousel.tsx +184 -0
- package/src/components/ui/Collapsible/Collapsible.mdx +53 -0
- package/src/components/ui/Collapsible/Collapsible.spec.tsx +68 -0
- package/src/components/ui/Collapsible/Collapsible.stories.tsx +48 -0
- package/src/components/ui/Collapsible/Collapsible.tsx +139 -0
- package/src/components/ui/DescriptionList/DescriptionList.mdx +50 -0
- package/src/components/ui/DescriptionList/DescriptionList.spec.tsx +37 -0
- package/src/components/ui/DescriptionList/DescriptionList.stories.tsx +48 -0
- package/src/components/ui/DescriptionList/DescriptionList.tsx +54 -0
- package/src/components/ui/Link/Link.mdx +53 -0
- package/src/components/ui/Link/Link.spec.tsx +35 -0
- package/src/components/ui/Link/Link.stories.tsx +46 -0
- package/src/components/ui/Link/Link.tsx +48 -0
- package/src/components/ui/Loading/Loading.spec.tsx +24 -0
- package/src/components/ui/Loading/Loading.stories.tsx +31 -0
- package/src/components/ui/Loading/Loading.tsx +29 -0
- package/src/components/ui/NotificationBanner/NotificationBanner.spec.tsx +85 -0
- package/src/components/ui/NotificationBanner/NotificationBanner.stories.tsx +37 -0
- package/src/components/ui/NotificationBanner/NotificationBanner.tsx +76 -0
- package/src/components/ui/Pagination/Pagination.mdx +62 -0
- package/src/components/ui/Pagination/Pagination.spec.tsx +86 -0
- package/src/components/ui/Pagination/Pagination.stories.tsx +47 -0
- package/src/components/ui/Pagination/Pagination.tsx +157 -0
- package/src/components/ui/Progress/Progress.spec.tsx +56 -0
- package/src/components/ui/Progress/Progress.stories.tsx +34 -0
- package/src/components/ui/Progress/Progress.tsx +84 -0
- package/src/components/ui/ProgressCircle/ProgressCircle.mdx +63 -0
- package/src/components/ui/ProgressCircle/ProgressCircle.spec.tsx +39 -0
- package/src/components/ui/ProgressCircle/ProgressCircle.stories.tsx +36 -0
- package/src/components/ui/ProgressCircle/ProgressCircle.tsx +176 -0
- package/src/components/ui/Separator/Separator.spec.tsx +39 -0
- package/src/components/ui/Separator/Separator.stories.tsx +38 -0
- package/src/components/ui/Separator/Separator.tsx +32 -0
- package/src/components/ui/Skeleton/Skeleton.spec.tsx +39 -0
- package/src/components/ui/Skeleton/Skeleton.stories.tsx +31 -0
- package/src/components/ui/Skeleton/Skeleton.tsx +42 -0
- package/src/components/ui/Table/Table.mdx +72 -0
- package/src/components/ui/Table/Table.spec.tsx +83 -0
- package/src/components/ui/Table/Table.stories.tsx +55 -0
- package/src/components/ui/Table/Table.tsx +196 -0
- package/src/components/ui/Tabs/Tabs.mdx +65 -0
- package/src/components/ui/Tabs/Tabs.spec.tsx +65 -0
- package/src/components/ui/Tabs/Tabs.stories.tsx +57 -0
- package/src/components/ui/Tabs/Tabs.tsx +133 -0
- package/src/components/ui/Tooltip/Tooltip.mdx +47 -0
- package/src/components/ui/Tooltip/Tooltip.spec.tsx +37 -0
- package/src/components/ui/Tooltip/Tooltip.stories.tsx +38 -0
- package/src/components/ui/Tooltip/Tooltip.tsx +57 -0
- package/src/components/ui/Typography/Typography.mdx +56 -0
- package/src/components/ui/Typography/Typography.spec.tsx +49 -0
- package/src/components/ui/Typography/Typography.stories.tsx +36 -0
- package/src/components/ui/Typography/Typography.tsx +65 -0
- package/src/index.ts +86 -0
- package/src/native/ReactNavigationTheme.mdx +140 -0
- package/src/native/ReactNavigationTheme.spec.tsx +35 -0
- package/src/native/ReactNavigationTheme.ts +102 -0
- package/src/native/ThemeContext.spec.tsx +57 -0
- package/src/native/ThemeContext.tsx +48 -0
- package/src/native/tokens.ts +13 -0
- 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
|
+
};
|