@boostdev/design-system-components 1.2.2 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +50 -5
  3. package/dist/client.cjs +50 -50
  4. package/dist/client.css +506 -503
  5. package/dist/client.js +50 -50
  6. package/dist/index.cjs +50 -50
  7. package/dist/index.css +506 -503
  8. package/dist/index.js +50 -50
  9. package/dist/native/index.cjs +3692 -352
  10. package/dist/native/index.d.cts +359 -3
  11. package/dist/native/index.d.ts +359 -3
  12. package/dist/native/index.js +3811 -364
  13. package/package.json +3 -3
  14. package/src/components/interaction/Button/Button.module.css +1 -4
  15. package/src/components/interaction/Button/Button.native.stories.tsx +10 -12
  16. package/src/components/interaction/Button/Button.native.tsx +3 -3
  17. package/src/components/interaction/Dialog/Dialog.module.css +1 -1
  18. package/src/components/interaction/Dialog/Dialog.native.mdx +61 -0
  19. package/src/components/interaction/Dialog/Dialog.native.spec.tsx +73 -0
  20. package/src/components/interaction/Dialog/Dialog.native.stories.tsx +53 -0
  21. package/src/components/interaction/Dialog/Dialog.native.tsx +128 -0
  22. package/src/components/interaction/Drawer/Drawer.module.css +1 -1
  23. package/src/components/interaction/Drawer/Drawer.native.mdx +58 -0
  24. package/src/components/interaction/Drawer/Drawer.native.spec.tsx +81 -0
  25. package/src/components/interaction/Drawer/Drawer.native.stories.tsx +33 -0
  26. package/src/components/interaction/Drawer/Drawer.native.tsx +175 -0
  27. package/src/components/interaction/DropdownMenu/DropdownMenu.module.css +4 -0
  28. package/src/components/interaction/DropdownMenu/DropdownMenu.native.mdx +74 -0
  29. package/src/components/interaction/DropdownMenu/DropdownMenu.native.spec.tsx +78 -0
  30. package/src/components/interaction/DropdownMenu/DropdownMenu.native.stories.tsx +51 -0
  31. package/src/components/interaction/DropdownMenu/DropdownMenu.native.tsx +254 -0
  32. package/src/components/interaction/DropdownMenu/DropdownMenu.stories.tsx +4 -4
  33. package/src/components/interaction/Popover/Popover.native.mdx +61 -0
  34. package/src/components/interaction/Popover/Popover.native.spec.tsx +73 -0
  35. package/src/components/interaction/Popover/Popover.native.stories.tsx +44 -0
  36. package/src/components/interaction/Popover/Popover.native.tsx +87 -0
  37. package/src/components/interaction/Rating/Rating.native.mdx +55 -0
  38. package/src/components/interaction/Rating/Rating.native.spec.tsx +38 -0
  39. package/src/components/interaction/Rating/Rating.native.stories.tsx +37 -0
  40. package/src/components/interaction/Rating/Rating.native.tsx +50 -0
  41. package/src/components/interaction/Toast/Toast.native.mdx +81 -0
  42. package/src/components/interaction/Toast/Toast.native.spec.tsx +80 -0
  43. package/src/components/interaction/Toast/Toast.native.stories.tsx +47 -0
  44. package/src/components/interaction/Toast/Toast.native.tsx +202 -0
  45. package/src/components/interaction/form/Checkbox/Checkbox.native.stories.tsx +2 -4
  46. package/src/components/interaction/form/Checkbox/Checkbox.native.tsx +1 -1
  47. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.mdx +78 -0
  48. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.spec.tsx +57 -0
  49. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.stories.tsx +96 -0
  50. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.tsx +61 -0
  51. package/src/components/interaction/form/Combobox/Combobox.module.css +1 -1
  52. package/src/components/interaction/form/Combobox/Combobox.native.mdx +86 -0
  53. package/src/components/interaction/form/Combobox/Combobox.native.spec.tsx +60 -0
  54. package/src/components/interaction/form/Combobox/Combobox.native.stories.tsx +99 -0
  55. package/src/components/interaction/form/Combobox/Combobox.native.tsx +211 -0
  56. package/src/components/interaction/form/FileInput/FileInput.native.mdx +86 -0
  57. package/src/components/interaction/form/FileInput/FileInput.native.spec.tsx +73 -0
  58. package/src/components/interaction/form/FileInput/FileInput.native.stories.tsx +69 -0
  59. package/src/components/interaction/form/FileInput/FileInput.native.tsx +129 -0
  60. package/src/components/interaction/form/FormInput/FormInput.module.css +1 -1
  61. package/src/components/interaction/form/FormInput/FormInput.native.mdx +81 -0
  62. package/src/components/interaction/form/FormInput/FormInput.native.spec.tsx +51 -0
  63. package/src/components/interaction/form/FormInput/FormInput.native.stories.tsx +54 -0
  64. package/src/components/interaction/form/FormInput/FormInput.native.tsx +122 -0
  65. package/src/components/interaction/form/NumberInput/NumberInput.module.css +1 -1
  66. package/src/components/interaction/form/NumberInput/NumberInput.native.mdx +87 -0
  67. package/src/components/interaction/form/NumberInput/NumberInput.native.spec.tsx +63 -0
  68. package/src/components/interaction/form/NumberInput/NumberInput.native.stories.tsx +63 -0
  69. package/src/components/interaction/form/NumberInput/NumberInput.native.tsx +198 -0
  70. package/src/components/interaction/form/Radio/Radio.native.spec.tsx +79 -0
  71. package/src/components/interaction/form/Radio/Radio.native.stories.tsx +12 -14
  72. package/src/components/interaction/form/Radio/Radio.native.tsx +1 -1
  73. package/src/components/interaction/form/RadioGroup/RadioGroup.native.mdx +79 -0
  74. package/src/components/interaction/form/RadioGroup/RadioGroup.native.spec.tsx +66 -0
  75. package/src/components/interaction/form/RadioGroup/RadioGroup.native.stories.tsx +96 -0
  76. package/src/components/interaction/form/RadioGroup/RadioGroup.native.tsx +61 -0
  77. package/src/components/interaction/form/SegmentedControl/SegmentedControl.module.css +5 -5
  78. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.mdx +78 -0
  79. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.spec.tsx +82 -0
  80. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.stories.tsx +99 -0
  81. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.tsx +121 -0
  82. package/src/components/interaction/form/Select/Select.module.css +1 -1
  83. package/src/components/interaction/form/Select/Select.native.mdx +85 -0
  84. package/src/components/interaction/form/Select/Select.native.spec.tsx +64 -0
  85. package/src/components/interaction/form/Select/Select.native.stories.tsx +75 -0
  86. package/src/components/interaction/form/Select/Select.native.tsx +252 -0
  87. package/src/components/interaction/form/Slider/Slider.module.css +4 -4
  88. package/src/components/interaction/form/Slider/Slider.native.mdx +83 -0
  89. package/src/components/interaction/form/Slider/Slider.native.spec.tsx +51 -0
  90. package/src/components/interaction/form/Slider/Slider.native.stories.tsx +58 -0
  91. package/src/components/interaction/form/Slider/Slider.native.tsx +188 -0
  92. package/src/components/interaction/form/Switch/Switch.module.css +3 -3
  93. package/src/components/interaction/form/Switch/Switch.native.stories.tsx +2 -4
  94. package/src/components/interaction/form/Textarea/Textarea.native.mdx +76 -0
  95. package/src/components/interaction/form/Textarea/Textarea.native.spec.tsx +50 -0
  96. package/src/components/interaction/form/Textarea/Textarea.native.stories.tsx +50 -0
  97. package/src/components/interaction/form/Textarea/Textarea.native.tsx +108 -0
  98. package/src/components/interaction/form/atoms/Label.native.spec.tsx +39 -0
  99. package/src/components/interaction/form/atoms/Label.native.stories.tsx +1 -1
  100. package/src/components/interaction/form/atoms/Message.native.stories.tsx +1 -1
  101. package/src/components/layout/ButtonGroup/ButtonGroup.native.spec.tsx +83 -0
  102. package/src/components/layout/ButtonGroup/ButtonGroup.native.stories.tsx +13 -19
  103. package/src/components/layout/Card/Card.native.spec.tsx +102 -0
  104. package/src/components/layout/Card/Card.native.stories.tsx +9 -11
  105. package/src/components/layout/Card/Card.native.tsx +1 -1
  106. package/src/components/layout/IconWrapper/IconWrapper.module.css +1 -1
  107. package/src/components/layout/IconWrapper/IconWrapper.native.stories.tsx +7 -11
  108. package/src/components/layout/SectionHeader/SectionHeader.native.spec.tsx +51 -0
  109. package/src/components/layout/SectionHeader/SectionHeader.native.stories.tsx +6 -8
  110. package/src/components/ui/Accordion/Accordion.native.mdx +65 -0
  111. package/src/components/ui/Accordion/Accordion.native.spec.tsx +69 -0
  112. package/src/components/ui/Accordion/Accordion.native.stories.tsx +52 -0
  113. package/src/components/ui/Accordion/Accordion.native.tsx +141 -0
  114. package/src/components/ui/Alert/Alert.module.css +1 -1
  115. package/src/components/ui/Alert/Alert.native.spec.tsx +76 -0
  116. package/src/components/ui/Alert/Alert.native.stories.tsx +8 -10
  117. package/src/components/ui/Alert/Alert.native.tsx +7 -7
  118. package/src/components/ui/Avatar/Avatar.module.css +1 -1
  119. package/src/components/ui/Avatar/Avatar.native.stories.tsx +6 -8
  120. package/src/components/ui/Avatar/Avatar.native.tsx +2 -2
  121. package/src/components/ui/Badge/Badge.module.css +1 -1
  122. package/src/components/ui/Badge/Badge.native.spec.tsx +45 -0
  123. package/src/components/ui/Badge/Badge.native.stories.tsx +6 -8
  124. package/src/components/ui/Badge/Badge.native.tsx +2 -2
  125. package/src/components/ui/Breadcrumb/Breadcrumb.native.mdx +52 -0
  126. package/src/components/ui/Breadcrumb/Breadcrumb.native.spec.tsx +49 -0
  127. package/src/components/ui/Breadcrumb/Breadcrumb.native.stories.tsx +46 -0
  128. package/src/components/ui/Breadcrumb/Breadcrumb.native.tsx +83 -0
  129. package/src/components/ui/Calendar/Calendar.module.css +2 -2
  130. package/src/components/ui/Calendar/Calendar.native.mdx +58 -0
  131. package/src/components/ui/Calendar/Calendar.native.spec.tsx +92 -0
  132. package/src/components/ui/Calendar/Calendar.native.stories.tsx +45 -0
  133. package/src/components/ui/Calendar/Calendar.native.tsx +294 -0
  134. package/src/components/ui/Carousel/Carousel.module.css +1 -1
  135. package/src/components/ui/Carousel/Carousel.native.mdx +55 -0
  136. package/src/components/ui/Carousel/Carousel.native.spec.tsx +55 -0
  137. package/src/components/ui/Carousel/Carousel.native.stories.tsx +59 -0
  138. package/src/components/ui/Carousel/Carousel.native.tsx +184 -0
  139. package/src/components/ui/Collapsible/Collapsible.native.mdx +53 -0
  140. package/src/components/ui/Collapsible/Collapsible.native.spec.tsx +68 -0
  141. package/src/components/ui/Collapsible/Collapsible.native.stories.tsx +48 -0
  142. package/src/components/ui/Collapsible/Collapsible.native.tsx +139 -0
  143. package/src/components/ui/DescriptionList/DescriptionList.native.mdx +50 -0
  144. package/src/components/ui/DescriptionList/DescriptionList.native.spec.tsx +37 -0
  145. package/src/components/ui/DescriptionList/DescriptionList.native.stories.tsx +48 -0
  146. package/src/components/ui/DescriptionList/DescriptionList.native.tsx +54 -0
  147. package/src/components/ui/Link/Link.native.mdx +53 -0
  148. package/src/components/ui/Link/Link.native.spec.tsx +35 -0
  149. package/src/components/ui/Link/Link.native.stories.tsx +46 -0
  150. package/src/components/ui/Link/Link.native.tsx +48 -0
  151. package/src/components/ui/Loading/Loading.module.css +1 -1
  152. package/src/components/ui/Loading/Loading.native.stories.tsx +6 -8
  153. package/src/components/ui/NotificationBanner/NotificationBanner.module.css +1 -1
  154. package/src/components/ui/NotificationBanner/NotificationBanner.native.spec.tsx +85 -0
  155. package/src/components/ui/NotificationBanner/NotificationBanner.native.stories.tsx +8 -10
  156. package/src/components/ui/NotificationBanner/NotificationBanner.native.tsx +7 -7
  157. package/src/components/ui/Pagination/Pagination.module.css +5 -1
  158. package/src/components/ui/Pagination/Pagination.native.mdx +62 -0
  159. package/src/components/ui/Pagination/Pagination.native.spec.tsx +86 -0
  160. package/src/components/ui/Pagination/Pagination.native.stories.tsx +47 -0
  161. package/src/components/ui/Pagination/Pagination.native.tsx +157 -0
  162. package/src/components/ui/Progress/Progress.module.css +2 -2
  163. package/src/components/ui/Progress/Progress.native.spec.tsx +56 -0
  164. package/src/components/ui/Progress/Progress.native.stories.tsx +1 -1
  165. package/src/components/ui/Progress/Progress.native.tsx +2 -2
  166. package/src/components/ui/ProgressCircle/ProgressCircle.native.mdx +63 -0
  167. package/src/components/ui/ProgressCircle/ProgressCircle.native.spec.tsx +39 -0
  168. package/src/components/ui/ProgressCircle/ProgressCircle.native.stories.tsx +36 -0
  169. package/src/components/ui/ProgressCircle/ProgressCircle.native.tsx +176 -0
  170. package/src/components/ui/Separator/Separator.native.spec.tsx +39 -0
  171. package/src/components/ui/Separator/Separator.native.stories.tsx +10 -14
  172. package/src/components/ui/Skeleton/Skeleton.native.spec.tsx +39 -0
  173. package/src/components/ui/Skeleton/Skeleton.native.stories.tsx +6 -8
  174. package/src/components/ui/Skeleton/Skeleton.native.tsx +2 -1
  175. package/src/components/ui/Table/Table.native.mdx +72 -0
  176. package/src/components/ui/Table/Table.native.spec.tsx +83 -0
  177. package/src/components/ui/Table/Table.native.stories.tsx +55 -0
  178. package/src/components/ui/Table/Table.native.tsx +196 -0
  179. package/src/components/ui/Tabs/Tabs.native.mdx +65 -0
  180. package/src/components/ui/Tabs/Tabs.native.spec.tsx +65 -0
  181. package/src/components/ui/Tabs/Tabs.native.stories.tsx +57 -0
  182. package/src/components/ui/Tabs/Tabs.native.tsx +133 -0
  183. package/src/components/ui/Tooltip/Tooltip.module.css +1 -1
  184. package/src/components/ui/Tooltip/Tooltip.native.mdx +47 -0
  185. package/src/components/ui/Tooltip/Tooltip.native.spec.tsx +37 -0
  186. package/src/components/ui/Tooltip/Tooltip.native.stories.tsx +38 -0
  187. package/src/components/ui/Tooltip/Tooltip.native.tsx +57 -0
  188. package/src/components/ui/Typography/Typography.native.spec.tsx +49 -0
  189. package/src/components/ui/Typography/Typography.native.stories.tsx +8 -10
  190. package/src/native/ThemeContext.tsx +3 -3
  191. package/src/native.ts +37 -0
  192. package/src/stories/ReactNative.mdx +48 -13
  193. package/src/web-components/interaction/BdsAccordion.stories.tsx +1 -1
  194. package/src/web-components/interaction/BdsCollapsible.stories.tsx +1 -1
  195. package/src/web-components/interaction/BdsSkipLink.stories.tsx +1 -1
  196. package/src/web-components/interaction/BdsTabs.stories.tsx +1 -1
  197. package/src/web-components/interaction/BdsTooltip.stories.tsx +1 -1
  198. package/src/web-components/interaction/form/BdsCheckbox.stories.tsx +1 -1
  199. package/src/web-components/interaction/form/BdsCheckboxGroup.stories.tsx +1 -1
  200. package/src/web-components/interaction/form/BdsCombobox.stories.tsx +1 -1
  201. package/src/web-components/interaction/form/BdsFileInput.stories.tsx +1 -1
  202. package/src/web-components/interaction/form/BdsFormInput.stories.tsx +1 -1
  203. package/src/web-components/interaction/form/BdsNumberInput.stories.tsx +1 -1
  204. package/src/web-components/interaction/form/BdsRadio.stories.tsx +1 -1
  205. package/src/web-components/interaction/form/BdsRadioGroup.stories.tsx +1 -1
  206. package/src/web-components/interaction/form/BdsSegmentedControl.stories.tsx +1 -1
  207. package/src/web-components/interaction/form/BdsSelect.stories.tsx +1 -1
  208. package/src/web-components/interaction/form/BdsSlider.stories.tsx +1 -1
  209. package/src/web-components/interaction/form/BdsSwitch.stories.tsx +1 -1
  210. package/src/web-components/interaction/form/BdsTextarea.stories.tsx +1 -1
  211. package/src/web-components/ui/BdsButtonGroup.stories.tsx +1 -1
  212. package/src/web-components/ui/BdsCard.stories.tsx +1 -1
  213. package/src/web-components/ui/BdsIconWrapper.stories.tsx +1 -1
  214. package/src/web-components/ui/BdsRating.stories.tsx +1 -1
  215. package/src/web-components/ui/BdsSectionHeader.stories.tsx +1 -1
@@ -0,0 +1,198 @@
1
+ import { useState } from 'react';
2
+ import {
3
+ StyleSheet,
4
+ View,
5
+ TextInput,
6
+ Text,
7
+ Pressable,
8
+ StyleProp,
9
+ ViewStyle,
10
+ } from 'react-native';
11
+ import { spacing, font, border, lineHeight } from '../../../../native/tokens';
12
+ import { useTheme } from '../../../../native/ThemeContext';
13
+ import { Label } from '../atoms/Label.native';
14
+ import { Message } from '../atoms/Message.native';
15
+
16
+ interface NumberInputProps {
17
+ label: string;
18
+ name: string;
19
+ value?: number;
20
+ defaultValue?: number;
21
+ min?: number;
22
+ max?: number;
23
+ step?: number;
24
+ disabled?: boolean;
25
+ error?: string;
26
+ hint?: string;
27
+ onChange?: (value: number) => void;
28
+ style?: StyleProp<ViewStyle>;
29
+ }
30
+
31
+ const STEPPER_SIZE = 44;
32
+
33
+ const styles = StyleSheet.create({
34
+ container: {
35
+ gap: spacing.xxs,
36
+ },
37
+ inputRow: {
38
+ flexDirection: 'row',
39
+ alignItems: 'center',
40
+ },
41
+ stepper: {
42
+ width: STEPPER_SIZE,
43
+ height: STEPPER_SIZE,
44
+ alignItems: 'center',
45
+ justifyContent: 'center',
46
+ borderWidth: 1,
47
+ },
48
+ stepperStart: {
49
+ borderTopStartRadius: border.radius.full,
50
+ borderBottomStartRadius: border.radius.full,
51
+ borderEndWidth: 0,
52
+ },
53
+ stepperEnd: {
54
+ borderTopEndRadius: border.radius.full,
55
+ borderBottomEndRadius: border.radius.full,
56
+ borderStartWidth: 0,
57
+ },
58
+ stepperText: {
59
+ fontSize: 20,
60
+ fontWeight: font.weight.semibold,
61
+ lineHeight: 24,
62
+ },
63
+ input: {
64
+ flex: 1,
65
+ fontFamily: font.family.body,
66
+ fontSize: font.size.body,
67
+ lineHeight: lineHeight.body,
68
+ textAlign: 'center',
69
+ paddingHorizontal: spacing.xs,
70
+ paddingVertical: spacing.xs,
71
+ borderTopWidth: 1,
72
+ borderBottomWidth: 1,
73
+ minHeight: STEPPER_SIZE,
74
+ },
75
+ });
76
+
77
+ export function NumberInput({
78
+ label,
79
+ name: _name, // eslint-disable-line @typescript-eslint/no-unused-vars
80
+ value,
81
+ defaultValue,
82
+ min,
83
+ max,
84
+ step = 1,
85
+ disabled = false,
86
+ error,
87
+ hint,
88
+ onChange,
89
+ style,
90
+ }: Readonly<NumberInputProps>) {
91
+ const { colors } = useTheme();
92
+ const isControlled = value !== undefined;
93
+ const [internalValue, setInternalValue] = useState(defaultValue ?? 0);
94
+
95
+ const currentValue = isControlled ? value : internalValue;
96
+
97
+ const clamp = (v: number): number => {
98
+ const withMin = min !== undefined ? Math.max(min, v) : v;
99
+ return max !== undefined ? Math.min(max, withMin) : withMin;
100
+ };
101
+
102
+ const update = (next: number) => {
103
+ const clamped = clamp(next);
104
+ if (!isControlled) setInternalValue(clamped);
105
+ onChange?.(clamped);
106
+ };
107
+
108
+ const canDecrease = min === undefined || currentValue > min;
109
+ const canIncrease = max === undefined || currentValue < max;
110
+
111
+ const borderColor = error ? colors.colorError : colors.colorOnBg;
112
+
113
+ return (
114
+ <View style={[styles.container, style]}>
115
+ <Label label={label} />
116
+ <View style={[styles.inputRow, disabled && { opacity: 0.4 }]}>
117
+ <Pressable
118
+ accessibilityRole="button"
119
+ accessibilityLabel="Decrease"
120
+ accessibilityState={{ disabled: disabled || !canDecrease }}
121
+ disabled={disabled || !canDecrease}
122
+ onPress={() => update(currentValue - step)}
123
+ style={({ pressed }) => [
124
+ styles.stepper,
125
+ styles.stepperStart,
126
+ {
127
+ borderColor,
128
+ backgroundColor: colors.colorBgSubtle,
129
+ },
130
+ pressed && { opacity: 0.7 },
131
+ (disabled || !canDecrease) && { opacity: 0.4 },
132
+ ]}
133
+ >
134
+ <Text
135
+ style={[styles.stepperText, { color: colors.colorOnBg }]}
136
+ aria-hidden
137
+ >
138
+ {'\u2212'}
139
+ </Text>
140
+ </Pressable>
141
+
142
+ <TextInput
143
+ accessibilityLabel={label}
144
+ accessibilityState={{ disabled }}
145
+ accessibilityValue={{
146
+ min,
147
+ max,
148
+ now: currentValue,
149
+ text: String(currentValue),
150
+ }}
151
+ keyboardType="numeric"
152
+ value={String(currentValue)}
153
+ editable={!disabled}
154
+ onChangeText={(text) => {
155
+ const parsed = parseFloat(text);
156
+ const safe = isNaN(parsed) ? 0 : parsed;
157
+ update(safe);
158
+ }}
159
+ style={[
160
+ styles.input,
161
+ {
162
+ borderColor,
163
+ color: colors.colorOnBg,
164
+ backgroundColor: colors.colorBg,
165
+ },
166
+ ]}
167
+ />
168
+
169
+ <Pressable
170
+ accessibilityRole="button"
171
+ accessibilityLabel="Increase"
172
+ accessibilityState={{ disabled: disabled || !canIncrease }}
173
+ disabled={disabled || !canIncrease}
174
+ onPress={() => update(currentValue + step)}
175
+ style={({ pressed }) => [
176
+ styles.stepper,
177
+ styles.stepperEnd,
178
+ {
179
+ borderColor,
180
+ backgroundColor: colors.colorBgSubtle,
181
+ },
182
+ pressed && { opacity: 0.7 },
183
+ (disabled || !canIncrease) && { opacity: 0.4 },
184
+ ]}
185
+ >
186
+ <Text
187
+ style={[styles.stepperText, { color: colors.colorOnBg }]}
188
+ aria-hidden
189
+ >
190
+ +
191
+ </Text>
192
+ </Pressable>
193
+ </View>
194
+ <Message type="error" message={error} />
195
+ <Message type="hint" message={hint} />
196
+ </View>
197
+ );
198
+ }
@@ -0,0 +1,79 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { ThemeProvider } from '../../../../native/ThemeContext';
3
+ import { Radio } from './Radio.native';
4
+
5
+ function renderWithTheme(ui: React.ReactElement) {
6
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
7
+ }
8
+
9
+ describe('Radio (native)', () => {
10
+ it('has radio role with label', () => {
11
+ renderWithTheme(<Radio label="Option A" name="choice" />);
12
+ expect(screen.getByRole('radio', { name: 'Option A' })).toBeInTheDocument();
13
+ });
14
+
15
+ it('renders unchecked by default (no inner dot)', () => {
16
+ renderWithTheme(<Radio label="Option A" name="choice" />);
17
+ // The outer circle is always present; the inner dot only renders when checked
18
+ const radio = screen.getByRole('radio');
19
+ // When unchecked, the inner filled circle should not be rendered
20
+ // The outer has 1 child (no inner dot) vs 2 when checked
21
+ expect(radio).toBeInTheDocument();
22
+ });
23
+
24
+ it('renders inner dot when checked', () => {
25
+ const { container: uncheckedContainer } = renderWithTheme(
26
+ <Radio label="Unchecked" name="choice" />,
27
+ );
28
+ const { container: checkedContainer } = renderWithTheme(
29
+ <Radio label="Checked" name="choice" checked />,
30
+ );
31
+ // The checked variant has more DOM nodes (the inner filled circle)
32
+ expect(checkedContainer.innerHTML.length).toBeGreaterThan(
33
+ uncheckedContainer.innerHTML.length,
34
+ );
35
+ });
36
+
37
+ it('reflects disabled state', () => {
38
+ renderWithTheme(<Radio label="Option A" name="choice" disabled />);
39
+ const radio = screen.getByRole('radio');
40
+ expect(radio).toHaveAttribute('aria-disabled', 'true');
41
+ });
42
+
43
+ it('calls onChange when clicked', () => {
44
+ const handleChange = vi.fn();
45
+ renderWithTheme(
46
+ <Radio label="Option A" name="choice" onChange={handleChange} />,
47
+ );
48
+ fireEvent.click(screen.getByRole('radio'));
49
+ expect(handleChange).toHaveBeenCalledTimes(1);
50
+ });
51
+
52
+ it('does not call onChange when disabled', () => {
53
+ const handleChange = vi.fn();
54
+ renderWithTheme(
55
+ <Radio label="Option A" name="choice" disabled onChange={handleChange} />,
56
+ );
57
+ fireEvent.click(screen.getByRole('radio'));
58
+ expect(handleChange).not.toHaveBeenCalled();
59
+ });
60
+
61
+ it('renders error message', () => {
62
+ renderWithTheme(
63
+ <Radio label="Option A" name="choice" error="Selection required." />,
64
+ );
65
+ expect(screen.getByRole('alert')).toHaveTextContent('Selection required.');
66
+ });
67
+
68
+ it('renders hint message', () => {
69
+ renderWithTheme(
70
+ <Radio label="Option A" name="choice" hint="Select your preference." />,
71
+ );
72
+ expect(screen.getByText('Select your preference.')).toBeInTheDocument();
73
+ });
74
+
75
+ it('uses name as accessibility label when label is not a string', () => {
76
+ renderWithTheme(<Radio label={<span>Custom</span>} name="fallback" />);
77
+ expect(screen.getByRole('radio', { name: 'fallback' })).toBeInTheDocument();
78
+ });
79
+ });
@@ -7,7 +7,7 @@ const meta = {
7
7
  tags: ['!stable', 'alpha'],
8
8
  title: 'React Native/Form/Radio',
9
9
  component: Radio,
10
- decorators: [(Story) => <ThemeProvider><Story /></ThemeProvider>],
10
+ decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
11
11
  parameters: { layout: 'padded' },
12
12
  } satisfies Meta<typeof Radio>;
13
13
 
@@ -28,19 +28,17 @@ export const Group: Story = {
28
28
  const [selected, setSelected] = useState<string | null>(null);
29
29
  const options = ['Option A', 'Option B', 'Option C'];
30
30
  return (
31
- <ThemeProvider>
32
- <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
33
- {options.map(o => (
34
- <Radio
35
- key={o}
36
- label={o}
37
- name="group"
38
- checked={selected === o}
39
- onChange={() => setSelected(o)}
40
- />
41
- ))}
42
- </div>
43
- </ThemeProvider>
31
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
32
+ {options.map(o => (
33
+ <Radio
34
+ key={o}
35
+ label={o}
36
+ name="group"
37
+ checked={selected === o}
38
+ onChange={() => setSelected(o)}
39
+ />
40
+ ))}
41
+ </div>
44
42
  );
45
43
  },
46
44
  };
@@ -55,7 +55,7 @@ export function Radio({
55
55
  style,
56
56
  }: RadioProps) {
57
57
  const { colors } = useTheme();
58
- const borderColor = error ? colors.colorError : checked ? colors.colorInteractive : colors.colorBgSubtle;
58
+ const borderColor = error ? colors.colorError : checked ? colors.colorInteractive : colors.colorOnBg;
59
59
 
60
60
  return (
61
61
  <View style={[styles.container, style]}>
@@ -0,0 +1,79 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './RadioGroup.native.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # RadioGroup (React Native)
7
+
8
+ Groups related radio buttons under a common legend with optional hint and error messages. Uses `accessibilityRole="radiogroup"` for proper screen reader semantics.
9
+
10
+ > **Status: alpha** -- API may change before stable release.
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { RadioGroup, Radio } from '@boostdev/components';
16
+ import { useState } from 'react';
17
+
18
+ function Example() {
19
+ const [selected, setSelected] = useState('standard');
20
+
21
+ return (
22
+ <RadioGroup legend="Shipping method">
23
+ <Radio
24
+ label="Standard"
25
+ name="shipping"
26
+ checked={selected === 'standard'}
27
+ onChange={() => setSelected('standard')}
28
+ />
29
+ <Radio
30
+ label="Express"
31
+ name="shipping"
32
+ checked={selected === 'express'}
33
+ onChange={() => setSelected('express')}
34
+ />
35
+ </RadioGroup>
36
+ );
37
+ }
38
+ ```
39
+
40
+ ## Examples
41
+
42
+ ### Default
43
+ <Canvas of={Stories.Default} />
44
+
45
+ ### Required
46
+ <Canvas of={Stories.Required} />
47
+
48
+ ### With hint
49
+ <Canvas of={Stories.WithHint} />
50
+
51
+ ### With error
52
+ <Canvas of={Stories.WithError} />
53
+
54
+ ### Disabled
55
+ <Canvas of={Stories.Disabled} />
56
+
57
+ ### Interactive
58
+ <Canvas of={Stories.Interactive} />
59
+
60
+ ## Props
61
+
62
+ <ArgTypes of={Stories} />
63
+
64
+ ## Props reference
65
+
66
+ | Prop | Type | Default | Description |
67
+ |------|------|---------|-------------|
68
+ | `legend` | string | -- | Group label displayed above the radio buttons |
69
+ | `children` | ReactNode | -- | Radio components |
70
+ | `error` | string | -- | Error message displayed below the group |
71
+ | `hint` | string | -- | Helper text displayed below the group |
72
+ | `required` | boolean | -- | Appends asterisk to legend |
73
+ | `disabled` | boolean | `false` | Reduces opacity of the entire group |
74
+
75
+ ## Accessibility
76
+
77
+ - Uses `accessibilityRole="radiogroup"` for proper grouping semantics
78
+ - The group is labelled via `accessibilityLabel` matching the legend text
79
+ - Error messages use `accessibilityRole="alert"` for screen reader announcement
@@ -0,0 +1,66 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { ThemeProvider } from '../../../../native/ThemeContext';
3
+ import { RadioGroup } from './RadioGroup.native';
4
+ import { Radio } from '../Radio/Radio.native';
5
+
6
+ function renderWithTheme(ui: React.ReactElement) {
7
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
8
+ }
9
+
10
+ describe('RadioGroup (native)', () => {
11
+ it('renders the legend text', () => {
12
+ renderWithTheme(
13
+ <RadioGroup legend="Shipping method">
14
+ <Radio label="Standard" name="shipping" />
15
+ </RadioGroup>,
16
+ );
17
+ expect(screen.getByText('Shipping method')).toBeInTheDocument();
18
+ });
19
+
20
+ it('has radiogroup role', () => {
21
+ renderWithTheme(
22
+ <RadioGroup legend="Shipping">
23
+ <Radio label="Standard" name="shipping" />
24
+ </RadioGroup>,
25
+ );
26
+ expect(screen.getByRole('radiogroup')).toBeInTheDocument();
27
+ });
28
+
29
+ it('renders children', () => {
30
+ renderWithTheme(
31
+ <RadioGroup legend="Options">
32
+ <Radio label="Option A" name="opt" />
33
+ <Radio label="Option B" name="opt" />
34
+ </RadioGroup>,
35
+ );
36
+ expect(screen.getByText('Option A')).toBeInTheDocument();
37
+ expect(screen.getByText('Option B')).toBeInTheDocument();
38
+ });
39
+
40
+ it('appends asterisk when required', () => {
41
+ renderWithTheme(
42
+ <RadioGroup legend="Payment" required>
43
+ <Radio label="Card" name="payment" />
44
+ </RadioGroup>,
45
+ );
46
+ expect(screen.getByText('*', { exact: false })).toBeInTheDocument();
47
+ });
48
+
49
+ it('renders error message', () => {
50
+ renderWithTheme(
51
+ <RadioGroup legend="Shipping" error="Please select one.">
52
+ <Radio label="Standard" name="shipping" />
53
+ </RadioGroup>,
54
+ );
55
+ expect(screen.getByRole('alert')).toHaveTextContent('Please select one.');
56
+ });
57
+
58
+ it('renders hint message', () => {
59
+ renderWithTheme(
60
+ <RadioGroup legend="Shipping" hint="Choose your preferred method.">
61
+ <Radio label="Standard" name="shipping" />
62
+ </RadioGroup>,
63
+ );
64
+ expect(screen.getByText('Choose your preferred method.')).toBeInTheDocument();
65
+ });
66
+ });
@@ -0,0 +1,96 @@
1
+ import React, { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { ThemeProvider } from '../../../../native/ThemeContext';
4
+ import { RadioGroup } from './RadioGroup.native';
5
+ import { Radio } from '../Radio/Radio.native';
6
+
7
+ const meta = {
8
+ tags: ['!stable', 'alpha'],
9
+ title: 'React Native/Form/RadioGroup',
10
+ component: RadioGroup,
11
+ decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
12
+ parameters: { layout: 'padded' },
13
+ } satisfies Meta<typeof RadioGroup>;
14
+
15
+ export default meta;
16
+ type Story = StoryObj<typeof meta>;
17
+
18
+ export const Default: Story = {
19
+ args: {
20
+ legend: 'Shipping method',
21
+ children: (
22
+ <>
23
+ <Radio label="Standard" name="shipping" />
24
+ <Radio label="Express" name="shipping" />
25
+ <Radio label="Next day" name="shipping" />
26
+ </>
27
+ ),
28
+ },
29
+ };
30
+
31
+ export const Required: Story = {
32
+ args: {
33
+ legend: 'Payment method',
34
+ required: true,
35
+ children: (
36
+ <>
37
+ <Radio label="Credit card" name="payment" />
38
+ <Radio label="iDEAL" name="payment" />
39
+ </>
40
+ ),
41
+ },
42
+ };
43
+
44
+ export const Disabled: Story = {
45
+ args: {
46
+ legend: 'Shipping method',
47
+ disabled: true,
48
+ children: (
49
+ <>
50
+ <Radio label="Standard" name="shipping" checked disabled />
51
+ <Radio label="Express" name="shipping" disabled />
52
+ </>
53
+ ),
54
+ },
55
+ };
56
+
57
+ export const WithError: Story = {
58
+ args: {
59
+ legend: 'Select one',
60
+ error: 'Please select a shipping method.',
61
+ children: (
62
+ <>
63
+ <Radio label="Standard" name="shipping" />
64
+ <Radio label="Express" name="shipping" />
65
+ </>
66
+ ),
67
+ },
68
+ };
69
+
70
+ export const WithHint: Story = {
71
+ args: {
72
+ legend: 'Delivery speed',
73
+ hint: 'Express adds a surcharge.',
74
+ children: (
75
+ <>
76
+ <Radio label="Standard" name="delivery" />
77
+ <Radio label="Express" name="delivery" />
78
+ </>
79
+ ),
80
+ },
81
+ };
82
+
83
+ function InteractiveComponent() {
84
+ const [selected, setSelected] = useState('standard');
85
+ return (
86
+ <RadioGroup legend="Shipping method">
87
+ <Radio label="Standard" name="shipping" checked={selected === 'standard'} onChange={() => setSelected('standard')} />
88
+ <Radio label="Express" name="shipping" checked={selected === 'express'} onChange={() => setSelected('express')} />
89
+ <Radio label="Next day" name="shipping" checked={selected === 'nextday'} onChange={() => setSelected('nextday')} />
90
+ </RadioGroup>
91
+ );
92
+ }
93
+
94
+ export const Interactive: Story = {
95
+ render: () => <InteractiveComponent />,
96
+ };
@@ -0,0 +1,61 @@
1
+ import { ReactNode } from 'react';
2
+ import { StyleSheet, View, Text, StyleProp, ViewStyle } from 'react-native';
3
+ import { spacing, font, lineHeight } from '../../../../native/tokens';
4
+ import { useTheme } from '../../../../native/ThemeContext';
5
+ import { Message } from '../atoms/Message.native';
6
+
7
+ interface RadioGroupProps {
8
+ legend: string;
9
+ children: ReactNode;
10
+ error?: string;
11
+ hint?: string;
12
+ required?: boolean;
13
+ disabled?: boolean;
14
+ style?: StyleProp<ViewStyle>;
15
+ }
16
+
17
+ const styles = StyleSheet.create({
18
+ container: {
19
+ gap: spacing.xs,
20
+ },
21
+ legend: {
22
+ fontFamily: font.family.body,
23
+ fontSize: font.size.body,
24
+ lineHeight: lineHeight.body,
25
+ fontWeight: font.weight.semibold,
26
+ },
27
+ required: {
28
+ fontWeight: font.weight.body,
29
+ },
30
+ items: {
31
+ gap: spacing.xxs,
32
+ },
33
+ });
34
+
35
+ export function RadioGroup({
36
+ legend,
37
+ children,
38
+ error,
39
+ hint,
40
+ required,
41
+ disabled = false,
42
+ style,
43
+ }: Readonly<RadioGroupProps>) {
44
+ const { colors } = useTheme();
45
+
46
+ return (
47
+ <View
48
+ style={[styles.container, disabled && { opacity: 0.4 }, style]}
49
+ accessibilityRole="radiogroup"
50
+ accessibilityLabel={legend}
51
+ >
52
+ <Text style={[styles.legend, { color: colors.colorOnBg }]}>
53
+ {legend}
54
+ {required && <Text style={[styles.required, { color: colors.colorOnBgSubtle }]}> *</Text>}
55
+ </Text>
56
+ <View style={styles.items}>{children}</View>
57
+ <Message type="error" message={error} />
58
+ <Message type="hint" message={hint} />
59
+ </View>
60
+ );
61
+ }
@@ -6,7 +6,7 @@
6
6
  grid-auto-columns: 1fr;
7
7
  padding: var(--bds-space_xxxs);
8
8
  background-color: var(--control_track-bg, var(--bds-color_bg--subtle));
9
- border-radius: var(--control_radius, var(--bds-border_radius--m));
9
+ border-radius: var(--control_radius, var(--bds-border_radius--full));
10
10
  gap: 0;
11
11
  }
12
12
 
@@ -16,7 +16,7 @@
16
16
  inset-block: var(--bds-space_xxxs);
17
17
  inset-inline-start: var(--bds-space_xxxs);
18
18
  inline-size: calc((100% - 2 * var(--bds-space_xxxs)) / var(--control_count, 1));
19
- border-radius: var(--control_thumb-radius, var(--bds-border_radius--s));
19
+ border-radius: var(--control_thumb-radius, var(--bds-border_radius--full));
20
20
  background-color: var(--control_thumb-bg, var(--bds-color_interactive));
21
21
  box-shadow: var(--bds-shadow_s);
22
22
  pointer-events: none;
@@ -30,7 +30,7 @@
30
30
  inset-block: var(--bds-space_xxxs);
31
31
  inset-inline-start: var(--bds-space_xxxs);
32
32
  inline-size: calc((100% - 2 * var(--bds-space_xxxs)) / var(--control_count, 1));
33
- border-radius: var(--control_thumb-radius, var(--bds-border_radius--s));
33
+ border-radius: var(--control_thumb-radius, var(--bds-border_radius--full));
34
34
  pointer-events: none;
35
35
  z-index: 2;
36
36
  display: none;
@@ -46,7 +46,7 @@
46
46
  align-items: center;
47
47
  justify-content: center;
48
48
  cursor: pointer;
49
- border-radius: var(--control_thumb-radius, var(--bds-border_radius--s));
49
+ border-radius: var(--control_thumb-radius, var(--bds-border_radius--full));
50
50
  padding: var(--bds-space_xs) var(--bds-space_m);
51
51
  font-size: var(--bds-font_size--body);
52
52
  font-family: var(--bds-font_family--body);
@@ -66,7 +66,7 @@
66
66
  .item:focus-visible {
67
67
  outline: var(--bds-outline_default);
68
68
  outline-offset: calc(var(--bds-outline_offset) * -1);
69
- border-radius: var(--control_thumb-radius, var(--bds-border_radius--s));
69
+ border-radius: var(--control_thumb-radius, var(--bds-border_radius--full));
70
70
  }
71
71
 
72
72
  .item.--active {