@boostdev/design-system-components 1.2.2 → 1.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -0
- package/README.md +50 -5
- package/dist/client.cjs +50 -50
- package/dist/client.css +506 -503
- package/dist/client.js +50 -50
- package/dist/index.cjs +50 -50
- package/dist/index.css +506 -503
- package/dist/index.js +50 -50
- package/dist/native/index.cjs +3692 -352
- package/dist/native/index.d.cts +359 -3
- package/dist/native/index.d.ts +359 -3
- package/dist/native/index.js +3811 -364
- package/package.json +3 -3
- package/src/components/interaction/Button/Button.module.css +1 -4
- package/src/components/interaction/Button/Button.native.stories.tsx +10 -12
- package/src/components/interaction/Button/Button.native.tsx +3 -3
- package/src/components/interaction/Dialog/Dialog.module.css +1 -1
- package/src/components/interaction/Dialog/Dialog.native.mdx +61 -0
- package/src/components/interaction/Dialog/Dialog.native.spec.tsx +73 -0
- package/src/components/interaction/Dialog/Dialog.native.stories.tsx +53 -0
- package/src/components/interaction/Dialog/Dialog.native.tsx +128 -0
- package/src/components/interaction/Drawer/Drawer.module.css +1 -1
- package/src/components/interaction/Drawer/Drawer.native.mdx +58 -0
- package/src/components/interaction/Drawer/Drawer.native.spec.tsx +81 -0
- package/src/components/interaction/Drawer/Drawer.native.stories.tsx +33 -0
- package/src/components/interaction/Drawer/Drawer.native.tsx +175 -0
- package/src/components/interaction/DropdownMenu/DropdownMenu.module.css +4 -0
- package/src/components/interaction/DropdownMenu/DropdownMenu.native.mdx +74 -0
- package/src/components/interaction/DropdownMenu/DropdownMenu.native.spec.tsx +78 -0
- package/src/components/interaction/DropdownMenu/DropdownMenu.native.stories.tsx +51 -0
- package/src/components/interaction/DropdownMenu/DropdownMenu.native.tsx +254 -0
- package/src/components/interaction/DropdownMenu/DropdownMenu.stories.tsx +4 -4
- package/src/components/interaction/Popover/Popover.native.mdx +61 -0
- package/src/components/interaction/Popover/Popover.native.spec.tsx +73 -0
- package/src/components/interaction/Popover/Popover.native.stories.tsx +44 -0
- package/src/components/interaction/Popover/Popover.native.tsx +87 -0
- package/src/components/interaction/Rating/Rating.native.mdx +55 -0
- package/src/components/interaction/Rating/Rating.native.spec.tsx +38 -0
- package/src/components/interaction/Rating/Rating.native.stories.tsx +37 -0
- package/src/components/interaction/Rating/Rating.native.tsx +50 -0
- package/src/components/interaction/Toast/Toast.native.mdx +81 -0
- package/src/components/interaction/Toast/Toast.native.spec.tsx +80 -0
- package/src/components/interaction/Toast/Toast.native.stories.tsx +47 -0
- package/src/components/interaction/Toast/Toast.native.tsx +202 -0
- package/src/components/interaction/form/Checkbox/Checkbox.native.stories.tsx +2 -4
- package/src/components/interaction/form/Checkbox/Checkbox.native.tsx +1 -1
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.mdx +78 -0
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.spec.tsx +57 -0
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.stories.tsx +96 -0
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.tsx +61 -0
- package/src/components/interaction/form/Combobox/Combobox.module.css +1 -1
- package/src/components/interaction/form/Combobox/Combobox.native.mdx +86 -0
- package/src/components/interaction/form/Combobox/Combobox.native.spec.tsx +60 -0
- package/src/components/interaction/form/Combobox/Combobox.native.stories.tsx +99 -0
- package/src/components/interaction/form/Combobox/Combobox.native.tsx +211 -0
- package/src/components/interaction/form/FileInput/FileInput.native.mdx +86 -0
- package/src/components/interaction/form/FileInput/FileInput.native.spec.tsx +73 -0
- package/src/components/interaction/form/FileInput/FileInput.native.stories.tsx +69 -0
- package/src/components/interaction/form/FileInput/FileInput.native.tsx +129 -0
- package/src/components/interaction/form/FormInput/FormInput.module.css +1 -1
- package/src/components/interaction/form/FormInput/FormInput.native.mdx +81 -0
- package/src/components/interaction/form/FormInput/FormInput.native.spec.tsx +51 -0
- package/src/components/interaction/form/FormInput/FormInput.native.stories.tsx +54 -0
- package/src/components/interaction/form/FormInput/FormInput.native.tsx +122 -0
- package/src/components/interaction/form/NumberInput/NumberInput.module.css +1 -1
- package/src/components/interaction/form/NumberInput/NumberInput.native.mdx +87 -0
- package/src/components/interaction/form/NumberInput/NumberInput.native.spec.tsx +63 -0
- package/src/components/interaction/form/NumberInput/NumberInput.native.stories.tsx +63 -0
- package/src/components/interaction/form/NumberInput/NumberInput.native.tsx +198 -0
- package/src/components/interaction/form/Radio/Radio.native.spec.tsx +79 -0
- package/src/components/interaction/form/Radio/Radio.native.stories.tsx +12 -14
- package/src/components/interaction/form/Radio/Radio.native.tsx +1 -1
- package/src/components/interaction/form/RadioGroup/RadioGroup.native.mdx +79 -0
- package/src/components/interaction/form/RadioGroup/RadioGroup.native.spec.tsx +66 -0
- package/src/components/interaction/form/RadioGroup/RadioGroup.native.stories.tsx +96 -0
- package/src/components/interaction/form/RadioGroup/RadioGroup.native.tsx +61 -0
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.module.css +5 -5
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.mdx +78 -0
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.spec.tsx +82 -0
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.stories.tsx +99 -0
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.tsx +121 -0
- package/src/components/interaction/form/Select/Select.module.css +1 -1
- package/src/components/interaction/form/Select/Select.native.mdx +85 -0
- package/src/components/interaction/form/Select/Select.native.spec.tsx +64 -0
- package/src/components/interaction/form/Select/Select.native.stories.tsx +75 -0
- package/src/components/interaction/form/Select/Select.native.tsx +252 -0
- package/src/components/interaction/form/Slider/Slider.module.css +4 -4
- package/src/components/interaction/form/Slider/Slider.native.mdx +83 -0
- package/src/components/interaction/form/Slider/Slider.native.spec.tsx +51 -0
- package/src/components/interaction/form/Slider/Slider.native.stories.tsx +58 -0
- package/src/components/interaction/form/Slider/Slider.native.tsx +188 -0
- package/src/components/interaction/form/Switch/Switch.module.css +3 -3
- package/src/components/interaction/form/Switch/Switch.native.stories.tsx +2 -4
- package/src/components/interaction/form/Textarea/Textarea.native.mdx +76 -0
- package/src/components/interaction/form/Textarea/Textarea.native.spec.tsx +50 -0
- package/src/components/interaction/form/Textarea/Textarea.native.stories.tsx +50 -0
- package/src/components/interaction/form/Textarea/Textarea.native.tsx +108 -0
- package/src/components/interaction/form/atoms/Label.native.spec.tsx +39 -0
- package/src/components/interaction/form/atoms/Label.native.stories.tsx +1 -1
- package/src/components/interaction/form/atoms/Message.native.stories.tsx +1 -1
- package/src/components/layout/ButtonGroup/ButtonGroup.native.spec.tsx +83 -0
- package/src/components/layout/ButtonGroup/ButtonGroup.native.stories.tsx +13 -19
- package/src/components/layout/Card/Card.native.spec.tsx +102 -0
- package/src/components/layout/Card/Card.native.stories.tsx +9 -11
- package/src/components/layout/Card/Card.native.tsx +1 -1
- package/src/components/layout/IconWrapper/IconWrapper.module.css +1 -1
- package/src/components/layout/IconWrapper/IconWrapper.native.stories.tsx +7 -11
- package/src/components/layout/SectionHeader/SectionHeader.native.spec.tsx +51 -0
- package/src/components/layout/SectionHeader/SectionHeader.native.stories.tsx +6 -8
- package/src/components/ui/Accordion/Accordion.native.mdx +65 -0
- package/src/components/ui/Accordion/Accordion.native.spec.tsx +69 -0
- package/src/components/ui/Accordion/Accordion.native.stories.tsx +52 -0
- package/src/components/ui/Accordion/Accordion.native.tsx +141 -0
- package/src/components/ui/Alert/Alert.module.css +1 -1
- package/src/components/ui/Alert/Alert.native.spec.tsx +76 -0
- package/src/components/ui/Alert/Alert.native.stories.tsx +8 -10
- package/src/components/ui/Alert/Alert.native.tsx +7 -7
- package/src/components/ui/Avatar/Avatar.module.css +1 -1
- package/src/components/ui/Avatar/Avatar.native.stories.tsx +6 -8
- package/src/components/ui/Avatar/Avatar.native.tsx +2 -2
- package/src/components/ui/Badge/Badge.module.css +1 -1
- package/src/components/ui/Badge/Badge.native.spec.tsx +45 -0
- package/src/components/ui/Badge/Badge.native.stories.tsx +6 -8
- package/src/components/ui/Badge/Badge.native.tsx +2 -2
- package/src/components/ui/Breadcrumb/Breadcrumb.native.mdx +52 -0
- package/src/components/ui/Breadcrumb/Breadcrumb.native.spec.tsx +49 -0
- package/src/components/ui/Breadcrumb/Breadcrumb.native.stories.tsx +46 -0
- package/src/components/ui/Breadcrumb/Breadcrumb.native.tsx +83 -0
- package/src/components/ui/Calendar/Calendar.module.css +2 -2
- package/src/components/ui/Calendar/Calendar.native.mdx +58 -0
- package/src/components/ui/Calendar/Calendar.native.spec.tsx +92 -0
- package/src/components/ui/Calendar/Calendar.native.stories.tsx +45 -0
- package/src/components/ui/Calendar/Calendar.native.tsx +294 -0
- package/src/components/ui/Carousel/Carousel.module.css +1 -1
- package/src/components/ui/Carousel/Carousel.native.mdx +55 -0
- package/src/components/ui/Carousel/Carousel.native.spec.tsx +55 -0
- package/src/components/ui/Carousel/Carousel.native.stories.tsx +59 -0
- package/src/components/ui/Carousel/Carousel.native.tsx +184 -0
- package/src/components/ui/Collapsible/Collapsible.native.mdx +53 -0
- package/src/components/ui/Collapsible/Collapsible.native.spec.tsx +68 -0
- package/src/components/ui/Collapsible/Collapsible.native.stories.tsx +48 -0
- package/src/components/ui/Collapsible/Collapsible.native.tsx +139 -0
- package/src/components/ui/DescriptionList/DescriptionList.native.mdx +50 -0
- package/src/components/ui/DescriptionList/DescriptionList.native.spec.tsx +37 -0
- package/src/components/ui/DescriptionList/DescriptionList.native.stories.tsx +48 -0
- package/src/components/ui/DescriptionList/DescriptionList.native.tsx +54 -0
- package/src/components/ui/Link/Link.native.mdx +53 -0
- package/src/components/ui/Link/Link.native.spec.tsx +35 -0
- package/src/components/ui/Link/Link.native.stories.tsx +46 -0
- package/src/components/ui/Link/Link.native.tsx +48 -0
- package/src/components/ui/Loading/Loading.module.css +1 -1
- package/src/components/ui/Loading/Loading.native.stories.tsx +6 -8
- package/src/components/ui/NotificationBanner/NotificationBanner.module.css +1 -1
- package/src/components/ui/NotificationBanner/NotificationBanner.native.spec.tsx +85 -0
- package/src/components/ui/NotificationBanner/NotificationBanner.native.stories.tsx +8 -10
- package/src/components/ui/NotificationBanner/NotificationBanner.native.tsx +7 -7
- package/src/components/ui/Pagination/Pagination.module.css +5 -1
- package/src/components/ui/Pagination/Pagination.native.mdx +62 -0
- package/src/components/ui/Pagination/Pagination.native.spec.tsx +86 -0
- package/src/components/ui/Pagination/Pagination.native.stories.tsx +47 -0
- package/src/components/ui/Pagination/Pagination.native.tsx +157 -0
- package/src/components/ui/Progress/Progress.module.css +2 -2
- package/src/components/ui/Progress/Progress.native.spec.tsx +56 -0
- package/src/components/ui/Progress/Progress.native.stories.tsx +1 -1
- package/src/components/ui/Progress/Progress.native.tsx +2 -2
- package/src/components/ui/ProgressCircle/ProgressCircle.native.mdx +63 -0
- package/src/components/ui/ProgressCircle/ProgressCircle.native.spec.tsx +39 -0
- package/src/components/ui/ProgressCircle/ProgressCircle.native.stories.tsx +36 -0
- package/src/components/ui/ProgressCircle/ProgressCircle.native.tsx +176 -0
- package/src/components/ui/Separator/Separator.native.spec.tsx +39 -0
- package/src/components/ui/Separator/Separator.native.stories.tsx +10 -14
- package/src/components/ui/Skeleton/Skeleton.native.spec.tsx +39 -0
- package/src/components/ui/Skeleton/Skeleton.native.stories.tsx +6 -8
- package/src/components/ui/Skeleton/Skeleton.native.tsx +2 -1
- package/src/components/ui/Table/Table.native.mdx +72 -0
- package/src/components/ui/Table/Table.native.spec.tsx +83 -0
- package/src/components/ui/Table/Table.native.stories.tsx +55 -0
- package/src/components/ui/Table/Table.native.tsx +196 -0
- package/src/components/ui/Tabs/Tabs.native.mdx +65 -0
- package/src/components/ui/Tabs/Tabs.native.spec.tsx +65 -0
- package/src/components/ui/Tabs/Tabs.native.stories.tsx +57 -0
- package/src/components/ui/Tabs/Tabs.native.tsx +133 -0
- package/src/components/ui/Tooltip/Tooltip.module.css +1 -1
- package/src/components/ui/Tooltip/Tooltip.native.mdx +47 -0
- package/src/components/ui/Tooltip/Tooltip.native.spec.tsx +37 -0
- package/src/components/ui/Tooltip/Tooltip.native.stories.tsx +38 -0
- package/src/components/ui/Tooltip/Tooltip.native.tsx +57 -0
- package/src/components/ui/Typography/Typography.native.spec.tsx +49 -0
- package/src/components/ui/Typography/Typography.native.stories.tsx +8 -10
- package/src/native/ThemeContext.tsx +3 -3
- package/src/native.ts +37 -0
- package/src/stories/ReactNative.mdx +48 -13
- package/src/web-components/interaction/BdsAccordion.stories.tsx +1 -1
- package/src/web-components/interaction/BdsCollapsible.stories.tsx +1 -1
- package/src/web-components/interaction/BdsSkipLink.stories.tsx +1 -1
- package/src/web-components/interaction/BdsTabs.stories.tsx +1 -1
- package/src/web-components/interaction/BdsTooltip.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsCheckbox.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsCheckboxGroup.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsCombobox.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsFileInput.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsFormInput.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsNumberInput.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsRadio.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsRadioGroup.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsSegmentedControl.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsSelect.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsSlider.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsSwitch.stories.tsx +1 -1
- package/src/web-components/interaction/form/BdsTextarea.stories.tsx +1 -1
- package/src/web-components/ui/BdsButtonGroup.stories.tsx +1 -1
- package/src/web-components/ui/BdsCard.stories.tsx +1 -1
- package/src/web-components/ui/BdsIconWrapper.stories.tsx +1 -1
- package/src/web-components/ui/BdsRating.stories.tsx +1 -1
- package/src/web-components/ui/BdsSectionHeader.stories.tsx +1 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
|
|
2
|
+
import * as Stories from './SegmentedControl.native.stories';
|
|
3
|
+
|
|
4
|
+
<Meta of={Stories} />
|
|
5
|
+
|
|
6
|
+
# SegmentedControl (React Native)
|
|
7
|
+
|
|
8
|
+
A horizontal control with multiple segments, where each segment acts as a tab. Supports `outline` and `filled` variants with three size options.
|
|
9
|
+
|
|
10
|
+
> **Status: alpha** -- API may change before stable release.
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { SegmentedControl } from '@boostdev/components';
|
|
16
|
+
import { useState } from 'react';
|
|
17
|
+
|
|
18
|
+
function Example() {
|
|
19
|
+
const [index, setIndex] = useState(0);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<SegmentedControl
|
|
23
|
+
selectedIndex={index}
|
|
24
|
+
onValueChange={setIndex}
|
|
25
|
+
accessibilityLabel="View mode"
|
|
26
|
+
>
|
|
27
|
+
{'Day'}
|
|
28
|
+
{'Week'}
|
|
29
|
+
{'Month'}
|
|
30
|
+
</SegmentedControl>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Examples
|
|
36
|
+
|
|
37
|
+
### Outline (default)
|
|
38
|
+
<Canvas of={Stories.Outline} />
|
|
39
|
+
|
|
40
|
+
### Filled
|
|
41
|
+
<Canvas of={Stories.Filled} />
|
|
42
|
+
|
|
43
|
+
### Small
|
|
44
|
+
<Canvas of={Stories.Small} />
|
|
45
|
+
|
|
46
|
+
### Large
|
|
47
|
+
<Canvas of={Stories.Large} />
|
|
48
|
+
|
|
49
|
+
### Disabled
|
|
50
|
+
<Canvas of={Stories.Disabled} />
|
|
51
|
+
|
|
52
|
+
### Interactive
|
|
53
|
+
<Canvas of={Stories.Interactive} />
|
|
54
|
+
|
|
55
|
+
### Interactive filled
|
|
56
|
+
<Canvas of={Stories.InteractiveFilled} />
|
|
57
|
+
|
|
58
|
+
## Props
|
|
59
|
+
|
|
60
|
+
<ArgTypes of={Stories} />
|
|
61
|
+
|
|
62
|
+
## Props reference
|
|
63
|
+
|
|
64
|
+
| Prop | Type | Default | Description |
|
|
65
|
+
|------|------|---------|-------------|
|
|
66
|
+
| `selectedIndex` | number | `0` | Zero-based index of the active segment |
|
|
67
|
+
| `disabled` | boolean | `false` | Disables all segments |
|
|
68
|
+
| `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | Controls height and font size |
|
|
69
|
+
| `variant` | `'outline' \| 'filled'` | `'outline'` | Visual style of the control |
|
|
70
|
+
| `children` | ReactNode | -- | Segment labels (strings or elements) |
|
|
71
|
+
| `accessibilityLabel` | string | -- | Label for the tab group |
|
|
72
|
+
| `onValueChange` | `(index: number) => void` | -- | Called with the tapped segment index |
|
|
73
|
+
|
|
74
|
+
## Accessibility
|
|
75
|
+
|
|
76
|
+
- Uses `accessibilityRole="tablist"` on the container
|
|
77
|
+
- Each segment uses `accessibilityRole="tab"` with `accessibilityState.selected`
|
|
78
|
+
- Disabled state is communicated via `accessibilityState`
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
2
|
+
import { ThemeProvider } from '../../../../native/ThemeContext';
|
|
3
|
+
import { SegmentedControl } from './SegmentedControl.native';
|
|
4
|
+
|
|
5
|
+
function renderWithTheme(ui: React.ReactElement) {
|
|
6
|
+
return render(<ThemeProvider>{ui}</ThemeProvider>);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('SegmentedControl (native)', () => {
|
|
10
|
+
it('renders all segments', () => {
|
|
11
|
+
renderWithTheme(
|
|
12
|
+
<SegmentedControl accessibilityLabel="View">
|
|
13
|
+
{'Day'}
|
|
14
|
+
{'Week'}
|
|
15
|
+
{'Month'}
|
|
16
|
+
</SegmentedControl>,
|
|
17
|
+
);
|
|
18
|
+
expect(screen.getByText('Day')).toBeInTheDocument();
|
|
19
|
+
expect(screen.getByText('Week')).toBeInTheDocument();
|
|
20
|
+
expect(screen.getByText('Month')).toBeInTheDocument();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('has tablist role', () => {
|
|
24
|
+
renderWithTheme(
|
|
25
|
+
<SegmentedControl accessibilityLabel="View">
|
|
26
|
+
{'Day'}
|
|
27
|
+
{'Week'}
|
|
28
|
+
</SegmentedControl>,
|
|
29
|
+
);
|
|
30
|
+
expect(screen.getByRole('tablist')).toBeInTheDocument();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('renders tab roles for each segment', () => {
|
|
34
|
+
renderWithTheme(
|
|
35
|
+
<SegmentedControl accessibilityLabel="View">
|
|
36
|
+
{'Day'}
|
|
37
|
+
{'Week'}
|
|
38
|
+
</SegmentedControl>,
|
|
39
|
+
);
|
|
40
|
+
const tabs = screen.getAllByRole('tab');
|
|
41
|
+
expect(tabs).toHaveLength(2);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('calls onValueChange with the clicked index', () => {
|
|
45
|
+
const onValueChange = vi.fn();
|
|
46
|
+
renderWithTheme(
|
|
47
|
+
<SegmentedControl accessibilityLabel="View" onValueChange={onValueChange}>
|
|
48
|
+
{'Day'}
|
|
49
|
+
{'Week'}
|
|
50
|
+
{'Month'}
|
|
51
|
+
</SegmentedControl>,
|
|
52
|
+
);
|
|
53
|
+
fireEvent.click(screen.getByText('Week'));
|
|
54
|
+
expect(onValueChange).toHaveBeenCalledWith(1);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('does not call onValueChange when disabled', () => {
|
|
58
|
+
const onValueChange = vi.fn();
|
|
59
|
+
renderWithTheme(
|
|
60
|
+
<SegmentedControl accessibilityLabel="View" onValueChange={onValueChange} disabled>
|
|
61
|
+
{'Day'}
|
|
62
|
+
{'Week'}
|
|
63
|
+
</SegmentedControl>,
|
|
64
|
+
);
|
|
65
|
+
fireEvent.click(screen.getByText('Week'));
|
|
66
|
+
expect(onValueChange).not.toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('renders the selected segment with distinct styling', () => {
|
|
70
|
+
renderWithTheme(
|
|
71
|
+
<SegmentedControl accessibilityLabel="View" selectedIndex={1}>
|
|
72
|
+
{'Day'}
|
|
73
|
+
{'Week'}
|
|
74
|
+
</SegmentedControl>,
|
|
75
|
+
);
|
|
76
|
+
// Verify both tabs render and the selected one exists
|
|
77
|
+
const tabs = screen.getAllByRole('tab');
|
|
78
|
+
expect(tabs).toHaveLength(2);
|
|
79
|
+
// The selected tab text is rendered
|
|
80
|
+
expect(screen.getByText('Week')).toBeInTheDocument();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import { ThemeProvider } from '../../../../native/ThemeContext';
|
|
4
|
+
import { SegmentedControl } from './SegmentedControl.native';
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
tags: ['!stable', 'alpha'],
|
|
8
|
+
title: 'React Native/Form/SegmentedControl',
|
|
9
|
+
component: SegmentedControl,
|
|
10
|
+
decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
|
|
11
|
+
parameters: { layout: 'padded' },
|
|
12
|
+
} satisfies Meta<typeof SegmentedControl>;
|
|
13
|
+
|
|
14
|
+
export default meta;
|
|
15
|
+
type Story = StoryObj<typeof meta>;
|
|
16
|
+
|
|
17
|
+
export const Outline: Story = {
|
|
18
|
+
args: {
|
|
19
|
+
variant: 'outline',
|
|
20
|
+
selectedIndex: 0,
|
|
21
|
+
children: ['Day', 'Week', 'Month'],
|
|
22
|
+
accessibilityLabel: 'View mode',
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const Filled: Story = {
|
|
27
|
+
args: {
|
|
28
|
+
variant: 'filled',
|
|
29
|
+
selectedIndex: 1,
|
|
30
|
+
children: ['Day', 'Week', 'Month'],
|
|
31
|
+
accessibilityLabel: 'View mode',
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const Small: Story = {
|
|
36
|
+
args: {
|
|
37
|
+
size: 'small',
|
|
38
|
+
selectedIndex: 0,
|
|
39
|
+
children: ['On', 'Off'],
|
|
40
|
+
accessibilityLabel: 'Toggle',
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const Large: Story = {
|
|
45
|
+
args: {
|
|
46
|
+
size: 'large',
|
|
47
|
+
selectedIndex: 2,
|
|
48
|
+
children: ['First', 'Second', 'Third'],
|
|
49
|
+
accessibilityLabel: 'Selection',
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const Disabled: Story = {
|
|
54
|
+
args: {
|
|
55
|
+
selectedIndex: 0,
|
|
56
|
+
disabled: true,
|
|
57
|
+
children: ['Day', 'Week', 'Month'],
|
|
58
|
+
accessibilityLabel: 'View mode',
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function InteractiveComponent() {
|
|
63
|
+
const [index, setIndex] = useState(0);
|
|
64
|
+
return (
|
|
65
|
+
<SegmentedControl
|
|
66
|
+
selectedIndex={index}
|
|
67
|
+
onValueChange={setIndex}
|
|
68
|
+
accessibilityLabel="View mode"
|
|
69
|
+
>
|
|
70
|
+
{'Day'}
|
|
71
|
+
{'Week'}
|
|
72
|
+
{'Month'}
|
|
73
|
+
</SegmentedControl>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const Interactive: Story = {
|
|
78
|
+
render: () => <InteractiveComponent />,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
function InteractiveFilledComponent() {
|
|
82
|
+
const [index, setIndex] = useState(0);
|
|
83
|
+
return (
|
|
84
|
+
<SegmentedControl
|
|
85
|
+
variant="filled"
|
|
86
|
+
selectedIndex={index}
|
|
87
|
+
onValueChange={setIndex}
|
|
88
|
+
accessibilityLabel="Category"
|
|
89
|
+
>
|
|
90
|
+
{'All'}
|
|
91
|
+
{'Active'}
|
|
92
|
+
{'Completed'}
|
|
93
|
+
</SegmentedControl>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const InteractiveFilled: Story = {
|
|
98
|
+
render: () => <InteractiveFilledComponent />,
|
|
99
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Children, ReactNode } from 'react';
|
|
2
|
+
import { StyleSheet, View, Pressable, Text, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import { spacing, font, border } from '../../../../native/tokens';
|
|
4
|
+
import { useTheme } from '../../../../native/ThemeContext';
|
|
5
|
+
|
|
6
|
+
type SegmentedControlSize = 'small' | 'medium' | 'large';
|
|
7
|
+
type SegmentedControlVariant = 'outline' | 'filled';
|
|
8
|
+
|
|
9
|
+
interface SegmentedControlProps {
|
|
10
|
+
selectedIndex?: number;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
size?: SegmentedControlSize;
|
|
13
|
+
variant?: SegmentedControlVariant;
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
accessibilityLabel?: string;
|
|
16
|
+
onValueChange?: (index: number) => void;
|
|
17
|
+
style?: StyleProp<ViewStyle>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const sizeTokens: Record<SegmentedControlSize, { height: number; fontSize: number; px: number }> = {
|
|
21
|
+
small: { height: 32, fontSize: font.size.bodyS, px: spacing.s },
|
|
22
|
+
medium: { height: 40, fontSize: font.size.body, px: spacing.m },
|
|
23
|
+
large: { height: 48, fontSize: font.size.body, px: spacing.l },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const styles = StyleSheet.create({
|
|
27
|
+
track: {
|
|
28
|
+
flexDirection: 'row',
|
|
29
|
+
borderRadius: border.radius.full,
|
|
30
|
+
overflow: 'hidden',
|
|
31
|
+
},
|
|
32
|
+
item: {
|
|
33
|
+
flex: 1,
|
|
34
|
+
alignItems: 'center',
|
|
35
|
+
justifyContent: 'center',
|
|
36
|
+
minHeight: 44,
|
|
37
|
+
},
|
|
38
|
+
label: {
|
|
39
|
+
fontFamily: font.family.body,
|
|
40
|
+
fontWeight: font.weight.semibold,
|
|
41
|
+
textAlign: 'center',
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export function SegmentedControl({
|
|
46
|
+
selectedIndex = 0,
|
|
47
|
+
disabled = false,
|
|
48
|
+
size = 'medium',
|
|
49
|
+
variant = 'outline',
|
|
50
|
+
children,
|
|
51
|
+
accessibilityLabel,
|
|
52
|
+
onValueChange,
|
|
53
|
+
style,
|
|
54
|
+
}: Readonly<SegmentedControlProps>) {
|
|
55
|
+
const { colors } = useTheme();
|
|
56
|
+
const { height, fontSize, px } = sizeTokens[size];
|
|
57
|
+
const items = Children.toArray(children);
|
|
58
|
+
|
|
59
|
+
const trackStyle: ViewStyle = variant === 'filled'
|
|
60
|
+
? { backgroundColor: colors.colorBgSubtle }
|
|
61
|
+
: { backgroundColor: 'transparent', borderWidth: 1, borderColor: colors.colorBgSubtle };
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<View
|
|
65
|
+
style={[styles.track, trackStyle, { borderRadius: border.radius.full }, style]}
|
|
66
|
+
accessibilityRole="tablist"
|
|
67
|
+
accessibilityLabel={accessibilityLabel}
|
|
68
|
+
>
|
|
69
|
+
{items.map((child, index) => {
|
|
70
|
+
const isActive = index === selectedIndex;
|
|
71
|
+
|
|
72
|
+
const activeStyle: ViewStyle = variant === 'filled'
|
|
73
|
+
? { backgroundColor: colors.colorInteractive }
|
|
74
|
+
: { borderWidth: 2, borderColor: colors.colorInteractive };
|
|
75
|
+
|
|
76
|
+
const inactiveStyle: ViewStyle = variant === 'filled'
|
|
77
|
+
? {}
|
|
78
|
+
: {};
|
|
79
|
+
|
|
80
|
+
const textColor = variant === 'filled' && isActive
|
|
81
|
+
? colors.colorOnInteractive
|
|
82
|
+
: isActive
|
|
83
|
+
? colors.colorInteractive
|
|
84
|
+
: colors.colorOnBgSubtle;
|
|
85
|
+
|
|
86
|
+
// Extract label from child: if it's a string, use it directly.
|
|
87
|
+
// Otherwise render the child as-is.
|
|
88
|
+
const label = typeof child === 'string' || typeof child === 'number'
|
|
89
|
+
? String(child)
|
|
90
|
+
: null;
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<Pressable
|
|
94
|
+
key={index}
|
|
95
|
+
onPress={() => !disabled && onValueChange?.(index)}
|
|
96
|
+
disabled={disabled}
|
|
97
|
+
accessibilityRole="tab"
|
|
98
|
+
accessibilityLabel={label ?? undefined}
|
|
99
|
+
accessibilityState={{ selected: isActive, disabled }}
|
|
100
|
+
style={({ pressed }) => [
|
|
101
|
+
styles.item,
|
|
102
|
+
{ height, paddingHorizontal: px },
|
|
103
|
+
isActive ? activeStyle : inactiveStyle,
|
|
104
|
+
variant === 'filled' && isActive && { borderRadius: border.radius.full },
|
|
105
|
+
pressed && !disabled && { opacity: 0.7 },
|
|
106
|
+
disabled && { opacity: 0.4 },
|
|
107
|
+
]}
|
|
108
|
+
>
|
|
109
|
+
{label !== null ? (
|
|
110
|
+
<Text style={[styles.label, { fontSize, color: textColor }]}>
|
|
111
|
+
{label}
|
|
112
|
+
</Text>
|
|
113
|
+
) : (
|
|
114
|
+
child
|
|
115
|
+
)}
|
|
116
|
+
</Pressable>
|
|
117
|
+
);
|
|
118
|
+
})}
|
|
119
|
+
</View>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
padding-inline-end: var(--bds-space_xxl);
|
|
20
20
|
border: var(--select_border, none);
|
|
21
21
|
--bdc_color: currentcolor;
|
|
22
|
-
--bdc-outline_radius: var(--bds-border_radius--
|
|
22
|
+
--bdc-outline_radius: var(--bds-border_radius--full);
|
|
23
23
|
|
|
24
24
|
border-radius: var(--select_radius, var(--bdc-outline_radius));
|
|
25
25
|
box-shadow: inset 0 0 0 var(--bdc-outline_width) var(--bdc_color), var(--select_shadow, var(--bdc-outline_shadow));
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
|
|
2
|
+
import * as Stories from './Select.native.stories';
|
|
3
|
+
|
|
4
|
+
<Meta of={Stories} />
|
|
5
|
+
|
|
6
|
+
# Select (React Native)
|
|
7
|
+
|
|
8
|
+
Dropdown-style select component. Renders a trigger button that opens a bottom sheet modal with a scrollable list of options.
|
|
9
|
+
|
|
10
|
+
> **Status: alpha** — API may change before stable release.
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { Select } from '@boostdev/components';
|
|
16
|
+
import { useState } from 'react';
|
|
17
|
+
|
|
18
|
+
const options = [
|
|
19
|
+
{ value: 'apple', label: 'Apple' },
|
|
20
|
+
{ value: 'banana', label: 'Banana' },
|
|
21
|
+
{ value: 'cherry', label: 'Cherry' },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
function Example() {
|
|
25
|
+
const [value, setValue] = useState('');
|
|
26
|
+
return (
|
|
27
|
+
<Select
|
|
28
|
+
label="Fruit"
|
|
29
|
+
name="fruit"
|
|
30
|
+
options={options}
|
|
31
|
+
placeholder="Choose a fruit..."
|
|
32
|
+
value={value}
|
|
33
|
+
onChange={setValue}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Examples
|
|
40
|
+
|
|
41
|
+
### Default
|
|
42
|
+
<Canvas of={Stories.Default} />
|
|
43
|
+
|
|
44
|
+
### With placeholder
|
|
45
|
+
<Canvas of={Stories.WithPlaceholder} />
|
|
46
|
+
|
|
47
|
+
### With selected value
|
|
48
|
+
<Canvas of={Stories.WithValue} />
|
|
49
|
+
|
|
50
|
+
### With error
|
|
51
|
+
<Canvas of={Stories.WithError} />
|
|
52
|
+
|
|
53
|
+
### With hint
|
|
54
|
+
<Canvas of={Stories.WithHint} />
|
|
55
|
+
|
|
56
|
+
### Disabled
|
|
57
|
+
<Canvas of={Stories.Disabled} />
|
|
58
|
+
|
|
59
|
+
### Interactive
|
|
60
|
+
<Canvas of={Stories.Interactive} />
|
|
61
|
+
|
|
62
|
+
## Props
|
|
63
|
+
|
|
64
|
+
<ArgTypes of={Stories} />
|
|
65
|
+
|
|
66
|
+
## Props reference
|
|
67
|
+
|
|
68
|
+
| Prop | Type | Default | Description |
|
|
69
|
+
|------|------|---------|-------------|
|
|
70
|
+
| `label` | ReactNode | — | Visible label text |
|
|
71
|
+
| `name` | string | — | Field name (used for accessibility) |
|
|
72
|
+
| `options` | `SelectOption[]` | — | Array of `{ value, label, disabled? }` |
|
|
73
|
+
| `placeholder` | string | `'Select an option'` | Placeholder when no value is selected |
|
|
74
|
+
| `value` | string | — | Currently selected value |
|
|
75
|
+
| `error` | string | — | Error message displayed below the trigger |
|
|
76
|
+
| `hint` | string | — | Hint text displayed below the trigger |
|
|
77
|
+
| `disabled` | boolean | `false` | Prevents interaction, reduces opacity |
|
|
78
|
+
| `onChange` | `(value: string) => void` | — | Called with the selected option value |
|
|
79
|
+
|
|
80
|
+
## Accessibility
|
|
81
|
+
|
|
82
|
+
- Trigger announces the label and current value via `accessibilityLabel`
|
|
83
|
+
- `accessibilityState.expanded` reflects whether the modal is open
|
|
84
|
+
- Each option announces its label and selected state
|
|
85
|
+
- Disabled options are communicated via `accessibilityState.disabled`
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { ThemeProvider } from '../../../../native/ThemeContext';
|
|
3
|
+
import { Select } from './Select.native';
|
|
4
|
+
|
|
5
|
+
const options = [
|
|
6
|
+
{ value: 'apple', label: 'Apple' },
|
|
7
|
+
{ value: 'banana', label: 'Banana' },
|
|
8
|
+
{ value: 'cherry', label: 'Cherry' },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
function renderWithTheme(ui: React.ReactElement) {
|
|
12
|
+
return render(<ThemeProvider>{ui}</ThemeProvider>);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('Select (native)', () => {
|
|
16
|
+
it('renders the label', () => {
|
|
17
|
+
renderWithTheme(<Select label="Fruit" name="fruit" options={options} />);
|
|
18
|
+
expect(screen.getByText('Fruit')).toBeInTheDocument();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('shows placeholder when no value is selected', () => {
|
|
22
|
+
renderWithTheme(
|
|
23
|
+
<Select label="Fruit" name="fruit" options={options} placeholder="Choose..." />,
|
|
24
|
+
);
|
|
25
|
+
expect(screen.getByText('Choose...')).toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('shows the selected option label', () => {
|
|
29
|
+
renderWithTheme(
|
|
30
|
+
<Select label="Fruit" name="fruit" options={options} value="cherry" />,
|
|
31
|
+
);
|
|
32
|
+
expect(screen.getByText('Cherry')).toBeInTheDocument();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('shows error message', () => {
|
|
36
|
+
renderWithTheme(
|
|
37
|
+
<Select label="Fruit" name="fruit" options={options} error="Required." />,
|
|
38
|
+
);
|
|
39
|
+
expect(screen.getByRole('alert')).toHaveTextContent('Required.');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('shows hint message', () => {
|
|
43
|
+
renderWithTheme(
|
|
44
|
+
<Select label="Fruit" name="fruit" options={options} hint="Pick one." />,
|
|
45
|
+
);
|
|
46
|
+
expect(screen.getByText('Pick one.')).toBeInTheDocument();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('renders the trigger button with correct accessibility', () => {
|
|
50
|
+
renderWithTheme(
|
|
51
|
+
<Select label="Fruit" name="fruit" options={options} />,
|
|
52
|
+
);
|
|
53
|
+
const trigger = screen.getByRole('button', { name: /Fruit/ });
|
|
54
|
+
expect(trigger).toBeInTheDocument();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('applies disabled opacity', () => {
|
|
58
|
+
renderWithTheme(
|
|
59
|
+
<Select label="Fruit" name="fruit" options={options} disabled value="banana" />,
|
|
60
|
+
);
|
|
61
|
+
const trigger = screen.getByRole('button', { name: /Fruit/ });
|
|
62
|
+
expect(trigger).toBeDisabled();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import { ThemeProvider } from '../../../../native/ThemeContext';
|
|
4
|
+
import { Select } from './Select.native';
|
|
5
|
+
|
|
6
|
+
const fruitOptions = [
|
|
7
|
+
{ value: 'apple', label: 'Apple' },
|
|
8
|
+
{ value: 'banana', label: 'Banana' },
|
|
9
|
+
{ value: 'cherry', label: 'Cherry' },
|
|
10
|
+
{ value: 'dragonfruit', label: 'Dragonfruit' },
|
|
11
|
+
{ value: 'elderberry', label: 'Elderberry' },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const meta = {
|
|
15
|
+
tags: ['!stable', 'alpha'],
|
|
16
|
+
title: 'React Native/Form/Select',
|
|
17
|
+
component: Select,
|
|
18
|
+
decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
|
|
19
|
+
parameters: { layout: 'padded' },
|
|
20
|
+
} satisfies Meta<typeof Select>;
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
23
|
+
type Story = StoryObj<typeof meta>;
|
|
24
|
+
|
|
25
|
+
export const Default: Story = {
|
|
26
|
+
args: { label: 'Fruit', name: 'fruit', options: fruitOptions },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const WithPlaceholder: Story = {
|
|
30
|
+
args: { label: 'Fruit', name: 'fruit', options: fruitOptions, placeholder: 'Choose a fruit...' },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const WithValue: Story = {
|
|
34
|
+
args: { label: 'Fruit', name: 'fruit', options: fruitOptions, value: 'cherry' },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const WithError: Story = {
|
|
38
|
+
args: { label: 'Fruit', name: 'fruit', options: fruitOptions, error: 'Please select a fruit.' },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const WithHint: Story = {
|
|
42
|
+
args: { label: 'Fruit', name: 'fruit', options: fruitOptions, hint: 'Pick your favourite.' },
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const Disabled: Story = {
|
|
46
|
+
args: { label: 'Fruit', name: 'fruit', options: fruitOptions, disabled: true, value: 'banana' },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const WithDisabledOption: Story = {
|
|
50
|
+
args: {
|
|
51
|
+
label: 'Fruit',
|
|
52
|
+
name: 'fruit',
|
|
53
|
+
options: [
|
|
54
|
+
{ value: 'apple', label: 'Apple' },
|
|
55
|
+
{ value: 'banana', label: 'Banana', disabled: true },
|
|
56
|
+
{ value: 'cherry', label: 'Cherry' },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const Interactive: Story = {
|
|
62
|
+
render: () => {
|
|
63
|
+
const [value, setValue] = useState('');
|
|
64
|
+
return (
|
|
65
|
+
<Select
|
|
66
|
+
label="Favourite fruit"
|
|
67
|
+
name="fruit"
|
|
68
|
+
options={fruitOptions}
|
|
69
|
+
placeholder="Choose a fruit..."
|
|
70
|
+
value={value}
|
|
71
|
+
onChange={setValue}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
},
|
|
75
|
+
};
|