@oxy.so/bloom 3.0.0 → 3.1.0

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 (271) hide show
  1. package/docs/adoption-matrix.mdx +266 -0
  2. package/docs/checkbox.mdx +16 -0
  3. package/docs/composition.mdx +88 -16
  4. package/docs/date-picker.mdx +11 -0
  5. package/docs/field.mdx +35 -4
  6. package/docs/file-upload.mdx +13 -0
  7. package/docs/glass.mdx +15 -0
  8. package/docs/input-group.mdx +22 -0
  9. package/docs/input-otp.mdx +14 -0
  10. package/docs/layout.mdx +34 -0
  11. package/docs/migrating-to-3.0.mdx +43 -4
  12. package/docs/page-header.mdx +46 -1
  13. package/docs/phone-input.mdx +17 -0
  14. package/docs/radio.mdx +16 -0
  15. package/docs/rating.mdx +14 -0
  16. package/docs/segmented-control.mdx +16 -4
  17. package/docs/select.mdx +22 -0
  18. package/docs/slider.mdx +14 -0
  19. package/docs/stepper.mdx +3 -1
  20. package/docs/switch.mdx +14 -0
  21. package/docs/text-field.mdx +14 -0
  22. package/docs/textarea.mdx +20 -0
  23. package/lib/commonjs/bottom-sheet/BottomSheetBase.js +11 -4
  24. package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
  25. package/lib/commonjs/checkbox/Checkbox.js +24 -8
  26. package/lib/commonjs/checkbox/Checkbox.js.map +1 -1
  27. package/lib/commonjs/date-picker/TimeField.js +18 -6
  28. package/lib/commonjs/date-picker/TimeField.js.map +1 -1
  29. package/lib/commonjs/dialog/DialogContent.js +36 -29
  30. package/lib/commonjs/dialog/DialogContent.js.map +1 -1
  31. package/lib/commonjs/field/index.js +7 -0
  32. package/lib/commonjs/field/index.js.map +1 -1
  33. package/lib/commonjs/field/membership.js +105 -0
  34. package/lib/commonjs/field/membership.js.map +1 -0
  35. package/lib/commonjs/file-upload/FileUpload.js +17 -3
  36. package/lib/commonjs/file-upload/FileUpload.js.map +1 -1
  37. package/lib/commonjs/index.js +21 -0
  38. package/lib/commonjs/index.js.map +1 -1
  39. package/lib/commonjs/index.web.js +21 -0
  40. package/lib/commonjs/index.web.js.map +1 -1
  41. package/lib/commonjs/input-group/InputGroup.js +18 -5
  42. package/lib/commonjs/input-group/InputGroup.js.map +1 -1
  43. package/lib/commonjs/input-otp/InputOtp.js +20 -8
  44. package/lib/commonjs/input-otp/InputOtp.js.map +1 -1
  45. package/lib/commonjs/layout/index.js +7 -0
  46. package/lib/commonjs/layout/index.js.map +1 -1
  47. package/lib/commonjs/layout/screen-scope.js +61 -0
  48. package/lib/commonjs/layout/screen-scope.js.map +1 -0
  49. package/lib/commonjs/phone-input/PhoneInput.js +20 -8
  50. package/lib/commonjs/phone-input/PhoneInput.js.map +1 -1
  51. package/lib/commonjs/radio/Radio.js +35 -9
  52. package/lib/commonjs/radio/Radio.js.map +1 -1
  53. package/lib/commonjs/rating/RatingInput.js +14 -6
  54. package/lib/commonjs/rating/RatingInput.js.map +1 -1
  55. package/lib/commonjs/segmented-control/SegmentedControl.js +38 -8
  56. package/lib/commonjs/segmented-control/SegmentedControl.js.map +1 -1
  57. package/lib/commonjs/select/Select.js +15 -3
  58. package/lib/commonjs/select/Select.js.map +1 -1
  59. package/lib/commonjs/select/Select.web.js +13 -5
  60. package/lib/commonjs/select/Select.web.js.map +1 -1
  61. package/lib/commonjs/slider/Slider.js +17 -3
  62. package/lib/commonjs/slider/Slider.js.map +1 -1
  63. package/lib/commonjs/stepper/Stepper.js +16 -7
  64. package/lib/commonjs/stepper/Stepper.js.map +1 -1
  65. package/lib/commonjs/styles/styled-primitives.js.map +1 -1
  66. package/lib/commonjs/switch/Switch.js +23 -8
  67. package/lib/commonjs/switch/Switch.js.map +1 -1
  68. package/lib/commonjs/text-field/TextField.js +19 -4
  69. package/lib/commonjs/text-field/TextField.js.map +1 -1
  70. package/lib/commonjs/textarea/Textarea.js +39 -15
  71. package/lib/commonjs/textarea/Textarea.js.map +1 -1
  72. package/lib/module/bottom-sheet/BottomSheetBase.js +8 -1
  73. package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
  74. package/lib/module/checkbox/Checkbox.js +24 -8
  75. package/lib/module/checkbox/Checkbox.js.map +1 -1
  76. package/lib/module/date-picker/TimeField.js +19 -5
  77. package/lib/module/date-picker/TimeField.js.map +1 -1
  78. package/lib/module/dialog/DialogContent.js +37 -30
  79. package/lib/module/dialog/DialogContent.js.map +1 -1
  80. package/lib/module/field/index.js +1 -0
  81. package/lib/module/field/index.js.map +1 -1
  82. package/lib/module/field/membership.js +101 -0
  83. package/lib/module/field/membership.js.map +1 -0
  84. package/lib/module/file-upload/FileUpload.js +17 -3
  85. package/lib/module/file-upload/FileUpload.js.map +1 -1
  86. package/lib/module/index.js +2 -2
  87. package/lib/module/index.js.map +1 -1
  88. package/lib/module/index.web.js +2 -2
  89. package/lib/module/index.web.js.map +1 -1
  90. package/lib/module/input-group/InputGroup.js +19 -4
  91. package/lib/module/input-group/InputGroup.js.map +1 -1
  92. package/lib/module/input-otp/InputOtp.js +21 -7
  93. package/lib/module/input-otp/InputOtp.js.map +1 -1
  94. package/lib/module/layout/index.js +4 -1
  95. package/lib/module/layout/index.js.map +1 -1
  96. package/lib/module/layout/screen-scope.js +57 -0
  97. package/lib/module/layout/screen-scope.js.map +1 -0
  98. package/lib/module/phone-input/PhoneInput.js +22 -9
  99. package/lib/module/phone-input/PhoneInput.js.map +1 -1
  100. package/lib/module/radio/Radio.js +35 -9
  101. package/lib/module/radio/Radio.js.map +1 -1
  102. package/lib/module/rating/RatingInput.js +15 -5
  103. package/lib/module/rating/RatingInput.js.map +1 -1
  104. package/lib/module/segmented-control/SegmentedControl.js +38 -8
  105. package/lib/module/segmented-control/SegmentedControl.js.map +1 -1
  106. package/lib/module/select/Select.js +15 -3
  107. package/lib/module/select/Select.js.map +1 -1
  108. package/lib/module/select/Select.web.js +14 -4
  109. package/lib/module/select/Select.web.js.map +1 -1
  110. package/lib/module/slider/Slider.js +17 -3
  111. package/lib/module/slider/Slider.js.map +1 -1
  112. package/lib/module/stepper/Stepper.js +17 -6
  113. package/lib/module/stepper/Stepper.js.map +1 -1
  114. package/lib/module/styles/styled-primitives.js.map +1 -1
  115. package/lib/module/switch/Switch.js +23 -8
  116. package/lib/module/switch/Switch.js.map +1 -1
  117. package/lib/module/text-field/TextField.js +19 -4
  118. package/lib/module/text-field/TextField.js.map +1 -1
  119. package/lib/module/textarea/Textarea.js +39 -15
  120. package/lib/module/textarea/Textarea.js.map +1 -1
  121. package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  122. package/lib/typescript/commonjs/checkbox/Checkbox.d.ts.map +1 -1
  123. package/lib/typescript/commonjs/checkbox/types.d.ts +5 -0
  124. package/lib/typescript/commonjs/checkbox/types.d.ts.map +1 -1
  125. package/lib/typescript/commonjs/date-picker/TimeField.d.ts +1 -1
  126. package/lib/typescript/commonjs/date-picker/TimeField.d.ts.map +1 -1
  127. package/lib/typescript/commonjs/date-picker/types.d.ts +6 -3
  128. package/lib/typescript/commonjs/date-picker/types.d.ts.map +1 -1
  129. package/lib/typescript/commonjs/dialog/DialogContent.d.ts.map +1 -1
  130. package/lib/typescript/commonjs/field/index.d.ts +2 -0
  131. package/lib/typescript/commonjs/field/index.d.ts.map +1 -1
  132. package/lib/typescript/commonjs/field/membership.d.ts +60 -0
  133. package/lib/typescript/commonjs/field/membership.d.ts.map +1 -0
  134. package/lib/typescript/commonjs/file-upload/FileUpload.d.ts +1 -1
  135. package/lib/typescript/commonjs/file-upload/FileUpload.d.ts.map +1 -1
  136. package/lib/typescript/commonjs/floating/types.d.ts +13 -0
  137. package/lib/typescript/commonjs/floating/types.d.ts.map +1 -1
  138. package/lib/typescript/commonjs/index.d.ts +3 -3
  139. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  140. package/lib/typescript/commonjs/index.web.d.ts +3 -3
  141. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  142. package/lib/typescript/commonjs/input-group/InputGroup.d.ts +1 -1
  143. package/lib/typescript/commonjs/input-group/InputGroup.d.ts.map +1 -1
  144. package/lib/typescript/commonjs/input-otp/InputOtp.d.ts.map +1 -1
  145. package/lib/typescript/commonjs/input-otp/types.d.ts +7 -1
  146. package/lib/typescript/commonjs/input-otp/types.d.ts.map +1 -1
  147. package/lib/typescript/commonjs/layout/index.d.ts +4 -1
  148. package/lib/typescript/commonjs/layout/index.d.ts.map +1 -1
  149. package/lib/typescript/commonjs/layout/screen-scope.d.ts +40 -0
  150. package/lib/typescript/commonjs/layout/screen-scope.d.ts.map +1 -0
  151. package/lib/typescript/commonjs/phone-input/PhoneInput.d.ts.map +1 -1
  152. package/lib/typescript/commonjs/radio/Radio.d.ts +1 -1
  153. package/lib/typescript/commonjs/radio/Radio.d.ts.map +1 -1
  154. package/lib/typescript/commonjs/radio/types.d.ts +11 -3
  155. package/lib/typescript/commonjs/radio/types.d.ts.map +1 -1
  156. package/lib/typescript/commonjs/rating/RatingInput.d.ts +1 -1
  157. package/lib/typescript/commonjs/rating/RatingInput.d.ts.map +1 -1
  158. package/lib/typescript/commonjs/rating/types.d.ts +6 -3
  159. package/lib/typescript/commonjs/rating/types.d.ts.map +1 -1
  160. package/lib/typescript/commonjs/segmented-control/SegmentedControl.d.ts +18 -2
  161. package/lib/typescript/commonjs/segmented-control/SegmentedControl.d.ts.map +1 -1
  162. package/lib/typescript/commonjs/select/Select.d.ts.map +1 -1
  163. package/lib/typescript/commonjs/select/Select.web.d.ts.map +1 -1
  164. package/lib/typescript/commonjs/select/types.d.ts +8 -1
  165. package/lib/typescript/commonjs/select/types.d.ts.map +1 -1
  166. package/lib/typescript/commonjs/slider/Slider.d.ts.map +1 -1
  167. package/lib/typescript/commonjs/stepper/Stepper.d.ts +1 -1
  168. package/lib/typescript/commonjs/stepper/Stepper.d.ts.map +1 -1
  169. package/lib/typescript/commonjs/stepper/types.d.ts +8 -1
  170. package/lib/typescript/commonjs/stepper/types.d.ts.map +1 -1
  171. package/lib/typescript/commonjs/styles/styled-primitives.d.ts +16 -0
  172. package/lib/typescript/commonjs/styles/styled-primitives.d.ts.map +1 -1
  173. package/lib/typescript/commonjs/switch/Switch.d.ts.map +1 -1
  174. package/lib/typescript/commonjs/switch/types.d.ts +7 -0
  175. package/lib/typescript/commonjs/switch/types.d.ts.map +1 -1
  176. package/lib/typescript/commonjs/text-field/TextField.d.ts.map +1 -1
  177. package/lib/typescript/commonjs/textarea/Textarea.d.ts +1 -1
  178. package/lib/typescript/commonjs/textarea/Textarea.d.ts.map +1 -1
  179. package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  180. package/lib/typescript/module/checkbox/Checkbox.d.ts.map +1 -1
  181. package/lib/typescript/module/checkbox/types.d.ts +5 -0
  182. package/lib/typescript/module/checkbox/types.d.ts.map +1 -1
  183. package/lib/typescript/module/date-picker/TimeField.d.ts +1 -1
  184. package/lib/typescript/module/date-picker/TimeField.d.ts.map +1 -1
  185. package/lib/typescript/module/date-picker/types.d.ts +6 -3
  186. package/lib/typescript/module/date-picker/types.d.ts.map +1 -1
  187. package/lib/typescript/module/dialog/DialogContent.d.ts.map +1 -1
  188. package/lib/typescript/module/field/index.d.ts +2 -0
  189. package/lib/typescript/module/field/index.d.ts.map +1 -1
  190. package/lib/typescript/module/field/membership.d.ts +60 -0
  191. package/lib/typescript/module/field/membership.d.ts.map +1 -0
  192. package/lib/typescript/module/file-upload/FileUpload.d.ts +1 -1
  193. package/lib/typescript/module/file-upload/FileUpload.d.ts.map +1 -1
  194. package/lib/typescript/module/floating/types.d.ts +13 -0
  195. package/lib/typescript/module/floating/types.d.ts.map +1 -1
  196. package/lib/typescript/module/index.d.ts +3 -3
  197. package/lib/typescript/module/index.d.ts.map +1 -1
  198. package/lib/typescript/module/index.web.d.ts +3 -3
  199. package/lib/typescript/module/index.web.d.ts.map +1 -1
  200. package/lib/typescript/module/input-group/InputGroup.d.ts +1 -1
  201. package/lib/typescript/module/input-group/InputGroup.d.ts.map +1 -1
  202. package/lib/typescript/module/input-otp/InputOtp.d.ts.map +1 -1
  203. package/lib/typescript/module/input-otp/types.d.ts +7 -1
  204. package/lib/typescript/module/input-otp/types.d.ts.map +1 -1
  205. package/lib/typescript/module/layout/index.d.ts +4 -1
  206. package/lib/typescript/module/layout/index.d.ts.map +1 -1
  207. package/lib/typescript/module/layout/screen-scope.d.ts +40 -0
  208. package/lib/typescript/module/layout/screen-scope.d.ts.map +1 -0
  209. package/lib/typescript/module/phone-input/PhoneInput.d.ts.map +1 -1
  210. package/lib/typescript/module/radio/Radio.d.ts +1 -1
  211. package/lib/typescript/module/radio/Radio.d.ts.map +1 -1
  212. package/lib/typescript/module/radio/types.d.ts +11 -3
  213. package/lib/typescript/module/radio/types.d.ts.map +1 -1
  214. package/lib/typescript/module/rating/RatingInput.d.ts +1 -1
  215. package/lib/typescript/module/rating/RatingInput.d.ts.map +1 -1
  216. package/lib/typescript/module/rating/types.d.ts +6 -3
  217. package/lib/typescript/module/rating/types.d.ts.map +1 -1
  218. package/lib/typescript/module/segmented-control/SegmentedControl.d.ts +18 -2
  219. package/lib/typescript/module/segmented-control/SegmentedControl.d.ts.map +1 -1
  220. package/lib/typescript/module/select/Select.d.ts.map +1 -1
  221. package/lib/typescript/module/select/Select.web.d.ts.map +1 -1
  222. package/lib/typescript/module/select/types.d.ts +8 -1
  223. package/lib/typescript/module/select/types.d.ts.map +1 -1
  224. package/lib/typescript/module/slider/Slider.d.ts.map +1 -1
  225. package/lib/typescript/module/stepper/Stepper.d.ts +1 -1
  226. package/lib/typescript/module/stepper/Stepper.d.ts.map +1 -1
  227. package/lib/typescript/module/stepper/types.d.ts +8 -1
  228. package/lib/typescript/module/stepper/types.d.ts.map +1 -1
  229. package/lib/typescript/module/styles/styled-primitives.d.ts +16 -0
  230. package/lib/typescript/module/styles/styled-primitives.d.ts.map +1 -1
  231. package/lib/typescript/module/switch/Switch.d.ts.map +1 -1
  232. package/lib/typescript/module/switch/types.d.ts +7 -0
  233. package/lib/typescript/module/switch/types.d.ts.map +1 -1
  234. package/lib/typescript/module/text-field/TextField.d.ts.map +1 -1
  235. package/lib/typescript/module/textarea/Textarea.d.ts +1 -1
  236. package/lib/typescript/module/textarea/Textarea.d.ts.map +1 -1
  237. package/package.json +2 -1
  238. package/src/bottom-sheet/BottomSheetBase.tsx +6 -1
  239. package/src/checkbox/Checkbox.tsx +24 -8
  240. package/src/checkbox/types.ts +5 -0
  241. package/src/date-picker/TimeField.tsx +14 -3
  242. package/src/date-picker/types.ts +6 -3
  243. package/src/dialog/DialogContent.tsx +6 -2
  244. package/src/field/index.ts +2 -0
  245. package/src/field/membership.ts +164 -0
  246. package/src/file-upload/FileUpload.tsx +14 -3
  247. package/src/floating/types.ts +13 -0
  248. package/src/index.ts +9 -2
  249. package/src/index.web.ts +9 -2
  250. package/src/input-group/InputGroup.tsx +13 -2
  251. package/src/input-otp/InputOtp.tsx +19 -5
  252. package/src/input-otp/types.ts +7 -1
  253. package/src/layout/index.ts +4 -1
  254. package/src/layout/screen-scope.tsx +54 -0
  255. package/src/phone-input/PhoneInput.tsx +20 -8
  256. package/src/radio/Radio.tsx +32 -9
  257. package/src/radio/types.ts +11 -3
  258. package/src/rating/RatingInput.tsx +10 -3
  259. package/src/rating/types.ts +6 -3
  260. package/src/segmented-control/SegmentedControl.tsx +51 -8
  261. package/src/select/Select.tsx +14 -2
  262. package/src/select/Select.web.tsx +11 -2
  263. package/src/select/types.ts +8 -1
  264. package/src/slider/Slider.tsx +13 -3
  265. package/src/stepper/Stepper.tsx +12 -4
  266. package/src/stepper/types.ts +8 -1
  267. package/src/styles/styled-primitives.ts +16 -0
  268. package/src/switch/Switch.tsx +22 -9
  269. package/src/switch/types.ts +7 -0
  270. package/src/text-field/TextField.tsx +16 -4
  271. package/src/textarea/Textarea.tsx +38 -15
@@ -12,6 +12,7 @@ import { resolveButtonRamps } from '../button/shared';
12
12
  import { focusRingShadow, interactiveWebCss, useInteractiveWebCss } from '../styles/interactive-web-css';
13
13
  import type { WebCssStyle } from '../styles/web-view-style';
14
14
  import { RadioIndicator } from '../radio-indicator';
15
+ import { useFieldMembership } from '../field/membership';
15
16
  import { RadioCard } from './RadioCard';
16
17
  import type { RadioGroupProps, RadioProps } from './types';
17
18
 
@@ -96,9 +97,20 @@ const RadioComponent = function Radio<Value extends string = string>({
96
97
  style,
97
98
  labelStyle,
98
99
  accessibilityLabel,
100
+ nativeID,
99
101
  testID,
100
102
  }: RadioProps<Value>) {
101
103
  const theme = useTheme();
104
+ // ADJACENT label (see `field/membership.ts`); the field's `disabled` is a
105
+ // constraint, so a radio inside a disabled field cannot re-enable itself.
106
+ const field = useFieldMembership({
107
+ accessibilityLabel,
108
+ label,
109
+ labelPlacement: 'adjacent',
110
+ disabled,
111
+ nativeID,
112
+ });
113
+ const isDisabled = field.disabled;
102
114
  const ringOffset = useRingOffsetStyle();
103
115
  useInteractiveWebCss(STYLE_ID, BLOOM_RADIO_CSS);
104
116
  const sizeConfig = SIZE_CONFIG[size];
@@ -109,16 +121,16 @@ const RadioComponent = function Radio<Value extends string = string>({
109
121
  // Re-choosing the chosen option is a no-op. A radio, unlike a checkbox, has
110
122
  // no "off" — firing here would make a group's `onValueChange` report a
111
123
  // change that did not happen.
112
- if (disabled || selected) return;
124
+ if (isDisabled || selected) return;
113
125
  onSelect(value);
114
- }, [disabled, selected, onSelect, value]);
126
+ }, [isDisabled, selected, onSelect, value]);
115
127
 
116
128
  const rowStyle: WebCssStyle = {
117
129
  flexDirection: 'row',
118
130
  alignItems: 'flex-start',
119
131
  gap: LABEL_GAP,
120
132
  // `opacity-50` on the whole row.
121
- opacity: disabled ? DISABLED_OPACITY : 1,
133
+ opacity: isDisabled ? DISABLED_OPACITY : 1,
122
134
  // The `:focus-visible` ring colour, read by the adopted sheet.
123
135
  '--bloom-radio-ring': color ?? accent[500],
124
136
  ...ringOffset,
@@ -129,7 +141,9 @@ const RadioComponent = function Radio<Value extends string = string>({
129
141
  {...(IS_WEB ? ({ dataSet: { bloomRadio: '' } } as Record<string, unknown>) : {})}
130
142
  style={[rowStyle, style]}
131
143
  onPress={handlePress}
132
- disabled={disabled}
144
+ disabled={isDisabled}
145
+ nativeID={field.nativeID}
146
+ aria-describedby={field.describedBy}
133
147
  accessibilityRole="radio"
134
148
  // `aria-checked`, not `accessibilityState`: react-native-web never reads
135
149
  // the latter, so an option that set only it announced no state at all.
@@ -137,7 +151,7 @@ const RadioComponent = function Radio<Value extends string = string>({
137
151
  // this one prop is the spelling both platforms honour, and `disabled`
138
152
  // travels on the `disabled` prop, which both map.
139
153
  aria-checked={selected}
140
- accessibilityLabel={accessibilityLabel ?? label}
154
+ accessibilityLabel={field.accessibilityLabel}
141
155
  hitSlop={HIT_SLOP}
142
156
  testID={testID}
143
157
  >
@@ -196,12 +210,21 @@ const RadioGroupComponent = function RadioGroup<Value extends string = string>({
196
210
  variant = 'default',
197
211
  testID,
198
212
  }: RadioGroupProps<Value>) {
213
+ // The group is one control made of several, so a `Field` around it names the
214
+ // GROUP and disables every option — `multiple` is the field's side of that
215
+ // (`docs/field.mdx`), and each radio keeps its own id and its own name.
216
+ // `label` here is a NAME, not rendered text — the group draws no label of its
217
+ // own — so it goes in as the caller's name and outranks the field's.
218
+ const field = useFieldMembership({ accessibilityLabel: label, disabled });
219
+ const isDisabled = field.disabled;
199
220
  return (
200
221
  <View
201
222
  style={[{ gap: space.sm }, style]}
202
223
  accessibilityRole="radiogroup"
203
- accessibilityLabel={label}
204
- aria-label={label}
224
+ accessibilityLabel={field.accessibilityLabel}
225
+ aria-label={field.accessibilityLabel}
226
+ aria-describedby={field.describedBy}
227
+ aria-invalid={field.invalid || undefined}
205
228
  testID={testID}
206
229
  >
207
230
  {options.map((option) =>
@@ -213,7 +236,7 @@ const RadioGroupComponent = function RadioGroup<Value extends string = string>({
213
236
  onSelect={onValueChange}
214
237
  title={option.label ?? option.value}
215
238
  description={option.description}
216
- disabled={disabled || option.disabled === true}
239
+ disabled={isDisabled || option.disabled === true}
217
240
  color={color}
218
241
  testID={option.testID}
219
242
  />
@@ -226,7 +249,7 @@ const RadioGroupComponent = function RadioGroup<Value extends string = string>({
226
249
  label={option.label}
227
250
  description={option.description}
228
251
  size={size}
229
- disabled={disabled || option.disabled === true}
252
+ disabled={isDisabled || option.disabled === true}
230
253
  color={color}
231
254
  labelStyle={labelStyle}
232
255
  testID={option.testID}
@@ -30,6 +30,11 @@ export interface RadioProps<Value extends string = string> {
30
30
  style?: StyleProp<ViewStyle>;
31
31
  labelStyle?: StyleProp<TextStyle>;
32
32
  accessibilityLabel?: string;
33
+ /**
34
+ * The control's id. Inside a `Field` the field supplies one, so the label's
35
+ * `htmlFor` points at this control on web.
36
+ */
37
+ nativeID?: string;
33
38
  testID?: string;
34
39
  }
35
40
 
@@ -44,10 +49,13 @@ export interface RadioOption<Value extends string = string> {
44
49
 
45
50
  export interface RadioGroupProps<Value extends string = string> {
46
51
  /**
47
- * The group's accessible name. Required: a `radiogroup` with no name announces
48
- * a list of options and nothing about what is being chosen.
52
+ * The group's accessible name.
53
+ *
54
+ * A `radiogroup` with no name announces a list of options and nothing about
55
+ * what is being chosen, so this or an enclosing `Field`'s label has to supply
56
+ * one.
49
57
  */
50
- label: string;
58
+ label?: string;
51
59
  /** The selected value, or `undefined` for a group with nothing chosen yet. */
52
60
  value: Value | undefined;
53
61
  /** Called with the newly chosen value. */
@@ -11,6 +11,7 @@ import { webDataSet } from '../styles/web-data';
11
11
  import type { WebCssStyle } from '../styles/web-view-style';
12
12
  import { useTheme } from '../theme/use-theme';
13
13
  import type { RatingInputProps, RatingInputSize } from './types';
14
+ import { useFieldMembership } from '../field/membership';
14
15
 
15
16
  /**
16
17
  * The star PICKER — `Rating`'s counterpart for a review form. `Rating` draws a
@@ -83,13 +84,17 @@ function RatingInputComponent({
83
84
  onChange,
84
85
  max = 5,
85
86
  size = 'medium',
86
- disabled = false,
87
+ disabled: disabledProp = false,
87
88
  accessibilityLabel,
88
89
  formatStarLabel = defaultStarLabel,
89
90
  style,
90
91
  testID,
91
92
  }: RatingInputProps) {
92
93
  const theme = useTheme();
94
+ // A row of stars draws no words, so the `radiogroup`'s name is a prop or the
95
+ // enclosing `Field`'s label; the field's `disabled` freezes the whole row.
96
+ const field = useFieldMembership({ accessibilityLabel, disabled: disabledProp });
97
+ const disabled = field.disabled;
93
98
  const ringOffset = useRingOffsetStyle();
94
99
  useInteractiveWebCss(STYLE_ID, BLOOM_RATING_INPUT_CSS);
95
100
  const { accent, neutral } = useMemo(() => resolveButtonRamps(theme), [theme]);
@@ -158,8 +163,10 @@ function RatingInputComponent({
158
163
  <View
159
164
  testID={testID}
160
165
  accessibilityRole="radiogroup"
161
- accessibilityLabel={accessibilityLabel}
162
- aria-label={accessibilityLabel}
166
+ nativeID={field.nativeID}
167
+ accessibilityLabel={field.accessibilityLabel}
168
+ aria-label={field.accessibilityLabel}
169
+ aria-describedby={field.describedBy}
163
170
  aria-disabled={disabled || undefined}
164
171
  style={[
165
172
  {
@@ -51,10 +51,13 @@ export interface RatingInputProps {
51
51
  disabled?: boolean;
52
52
  /**
53
53
  * The NAME of what is being rated ("Overall rating", "Cleanliness").
54
- * Required: the group draws nothing but stars, so nothing else can tell a
55
- * screen reader which of several ratings this one is.
54
+ *
55
+ * The group draws nothing but stars, so this or an enclosing `Field`'s label
56
+ * is the only thing that can tell a screen reader which of several ratings
57
+ * this one is. With neither it warns once in development
58
+ * (`hooks/use-accessible-name-warning.ts`).
56
59
  */
57
- accessibilityLabel: string;
60
+ accessibilityLabel?: string;
58
61
  /**
59
62
  * Each star's own name, which is English by default (`"1 star"`,
60
63
  * `"4 stars"`). Pass a translated formatter.
@@ -27,6 +27,7 @@ import { borderRadius, DISABLED_OPACITY } from '../styles/tokens';
27
27
  import { NOT_DISABLED, interactiveWebCss, useInteractiveWebCss } from '../styles/interactive-web-css';
28
28
  import type { WebCssStyle } from '../styles/web-view-style';
29
29
  import { mixColor, resolveButtonRamps } from '../button/shared';
30
+ import { useFieldMembership } from '../field/membership';
30
31
 
31
32
  /**
32
33
  * A segmented control. Colours come from Bloom's theme through
@@ -136,6 +137,8 @@ const InternalContext = createContext<{
136
137
  size: SegmentedControlSize;
137
138
  variant: SegmentedControlVariant;
138
139
  palette: SegmentedPalette;
140
+ /** The group's disabled state. An item ORs it with its own. */
141
+ disabled: boolean;
139
142
  selectedValue: string;
140
143
  selectedPosition: { width: number; x: number } | null;
141
144
  onSelectValue: (
@@ -169,9 +172,17 @@ export function SegmentedControl<T extends string>({
169
172
  onChange,
170
173
  children,
171
174
  style,
175
+ disabled = false,
172
176
  accessibilityHint,
177
+ nativeID,
178
+ testID,
173
179
  }: {
174
- label: string;
180
+ /**
181
+ * The control's accessible name — it is a `radiogroup`/`tablist`, and the
182
+ * segment texts name the OPTIONS rather than the choice. Inside a `Field` the
183
+ * field's label supplies it.
184
+ */
185
+ label?: string;
175
186
  type: 'tabs' | 'radio';
176
187
  /** `medium` (default); `small` and `large` step around it. */
177
188
  size?: SegmentedControlSize;
@@ -181,10 +192,28 @@ export function SegmentedControl<T extends string>({
181
192
  onChange: (value: T) => void;
182
193
  children: React.ReactNode;
183
194
  style?: StyleProp<ViewStyle>;
195
+ /**
196
+ * Disables every segment.
197
+ *
198
+ * A CONSTRAINT the group owns: an item combines it with `||`, so a segment
199
+ * cannot re-enable itself, and a `Field disabled` around the control reaches
200
+ * the segments through it.
201
+ */
202
+ disabled?: boolean;
184
203
  accessibilityHint?: string;
204
+ /** The group's id. Supplied by an enclosing `Field` when there is one. */
205
+ nativeID?: string;
206
+ testID?: string;
185
207
  }) {
186
208
  const theme = useTheme();
187
209
  useInteractiveWebCss(STYLE_ID, SEGMENTED_CSS);
210
+ // The group is ONE control made of several segments, so a `Field` names the
211
+ // group and disables all of it. `label` is the group's name rather than
212
+ // rendered text, so the field's label only fills in what is missing.
213
+ // `label` is a NAME rather than rendered text (the segments carry the words),
214
+ // so it goes in as the caller's own name and outranks the field's.
215
+ const field = useFieldMembership({ accessibilityLabel: label, disabled, nativeID });
216
+ const isDisabled = field.disabled;
188
217
  const palette = useMemo(() => resolveSegmentedPalette(theme), [theme]);
189
218
  const [selectedPosition, setSelectedPosition] = useState<{
190
219
  width: number;
@@ -197,6 +226,7 @@ export function SegmentedControl<T extends string>({
197
226
  size,
198
227
  variant,
199
228
  palette,
229
+ disabled: isDisabled,
200
230
  selectedValue: value,
201
231
  selectedPosition,
202
232
  onSelectValue: (
@@ -219,7 +249,7 @@ export function SegmentedControl<T extends string>({
219
249
  });
220
250
  },
221
251
  };
222
- }, [value, selectedPosition, setSelectedPosition, onChange, type, size, variant, palette]);
252
+ }, [value, selectedPosition, setSelectedPosition, onChange, type, size, variant, palette, isDisabled]);
223
253
 
224
254
  const solid = variant === 'solid';
225
255
  const padding = solid ? TRACK_PADDING : 0;
@@ -231,8 +261,15 @@ export function SegmentedControl<T extends string>({
231
261
 
232
262
  return (
233
263
  <View
234
- accessibilityLabel={label}
264
+ testID={testID}
265
+ nativeID={field.nativeID}
266
+ accessibilityLabel={field.accessibilityLabel}
235
267
  accessibilityHint={accessibilityHint ?? ''}
268
+ aria-describedby={field.describedBy}
269
+ aria-invalid={field.invalid || undefined}
270
+ // A `View`, so `aria-disabled` is the only spelling that reaches web —
271
+ // there is no `disabled` prop for react-native-web to derive it from.
272
+ aria-disabled={isDisabled || undefined}
236
273
  style={[
237
274
  {
238
275
  position: 'relative',
@@ -320,10 +357,16 @@ export function SegmentedControlItem({
320
357
  }
321
358
  }, [needsUpdate]);
322
359
 
360
+ // The group's `disabled` is a constraint, so it is OR-ed with the item's own
361
+ // — a segment cannot re-enable itself inside a disabled group, and a `Field
362
+ // disabled` reaches every segment through the group.
363
+ const isDisabled = ctx.disabled || disabled === true;
364
+
323
365
  const onPress = useCallback(() => {
366
+ if (isDisabled) return;
324
367
  ctx.onSelectValue(value, position);
325
368
  onPressProp?.();
326
- }, [ctx, value, position, onPressProp]);
369
+ }, [isDisabled, ctx, value, position, onPressProp]);
327
370
 
328
371
  // We render the segment as a flat `Pressable` (not Bloom's `Button`)
329
372
  // for two reasons:
@@ -346,13 +389,13 @@ export function SegmentedControlItem({
346
389
  height: geometry.height,
347
390
  paddingHorizontal: geometry.paddingHorizontal,
348
391
  borderRadius: borderRadius.full,
349
- opacity: disabled ? DISABLED_OPACITY : 1,
392
+ opacity: isDisabled ? DISABLED_OPACITY : 1,
350
393
  '--bloom-segmented-ring': ctx.palette.ring,
351
394
  };
352
395
 
353
396
  const itemContext = useMemo(
354
- () => ({ active, hovered: hovered && !disabled }),
355
- [active, hovered, disabled],
397
+ () => ({ active, hovered: hovered && !isDisabled }),
398
+ [active, hovered, isDisabled],
356
399
  );
357
400
 
358
401
  return (
@@ -384,7 +427,7 @@ export function SegmentedControlItem({
384
427
  ? { 'aria-selected': active }
385
428
  : { 'aria-checked': active })}
386
429
  role={itemRole}
387
- disabled={disabled}
430
+ disabled={isDisabled}
388
431
  testID={testID}
389
432
  style={[itemStyle, style]}>
390
433
  <InternalItemContext.Provider value={itemContext}>
@@ -57,6 +57,7 @@ import type {
57
57
  SelectTriggerProps,
58
58
  SelectValueProps,
59
59
  } from './types';
60
+ import { useFieldMembership } from '../field/membership';
60
61
 
61
62
  // ---------------------------------------------------------------------------
62
63
  // Context
@@ -133,7 +134,15 @@ export function SelectTrigger({
133
134
  }: SelectTriggerProps) {
134
135
  const { control, size, disabled: rootDisabled } = useSelectContext();
135
136
  const palette = useMenuPalette();
136
- const isDisabled = disabled === true || rootDisabled === true;
137
+ // The trigger IS the select's control, so it takes the enclosing `Field`'s id,
138
+ // name, description and invalid state — and the field's `disabled` on top of
139
+ // the two it already combines. A `Popover` trigger deliberately does not do
140
+ // this (`floating/types.ts`): it is not the field's control.
141
+ const membership = useFieldMembership({
142
+ accessibilityLabel: label,
143
+ disabled: disabled === true || rootDisabled === true,
144
+ });
145
+ const isDisabled = membership.disabled;
137
146
  const t = palette.trigger;
138
147
 
139
148
  // The same field the web fork draws — a bordered white select trigger
@@ -171,9 +180,12 @@ export function SelectTrigger({
171
180
  handle={{
172
181
  onPress: () => control.open(),
173
182
  disabled: isDisabled,
174
- accessibilityLabel: label,
183
+ accessibilityLabel: membership.accessibilityLabel,
175
184
  accessibilityRole: 'button',
176
185
  'aria-haspopup': SELECT_TRIGGER_POPUP,
186
+ nativeID: membership.nativeID,
187
+ 'aria-describedby': membership.describedBy,
188
+ 'aria-invalid': membership.invalid || undefined,
177
189
  }}>
178
190
  {asChild ? children : field}
179
191
  </TriggerSlot>
@@ -59,6 +59,7 @@ import type {
59
59
  SelectTriggerProps,
60
60
  SelectValueProps,
61
61
  } from './types';
62
+ import { useFieldMembership } from '../field/membership';
62
63
 
63
64
  /** The trigger's `offset={4}`. */
64
65
  const SELECT_OFFSET = 4;
@@ -209,7 +210,12 @@ export function SelectTrigger({
209
210
  }: SelectTriggerProps) {
210
211
  const ctx = useSelectContext();
211
212
  const palette = useMenuPalette();
212
- const isDisabled = disabled === true || ctx.disabled === true;
213
+ // The trigger IS the select's control — see the native fork.
214
+ const membership = useFieldMembership({
215
+ accessibilityLabel: label,
216
+ disabled: disabled === true || ctx.disabled === true,
217
+ });
218
+ const isDisabled = membership.disabled;
213
219
  useEffect(() => {
214
220
  adoptStyleSheet(TRIGGER_STYLE_ID, TRIGGER_CSS);
215
221
  }, []);
@@ -261,10 +267,13 @@ export function SelectTrigger({
261
267
  // it rather than reopening it.
262
268
  onPress: () => (ctx.isOpen ? ctx.close() : ctx.open()),
263
269
  disabled: isDisabled,
264
- accessibilityLabel: label,
270
+ accessibilityLabel: membership.accessibilityLabel,
265
271
  accessibilityRole: 'button',
266
272
  'aria-haspopup': SELECT_TRIGGER_POPUP,
267
273
  'aria-expanded': ctx.isOpen,
274
+ nativeID: membership.nativeID,
275
+ 'aria-describedby': membership.describedBy,
276
+ 'aria-invalid': membership.invalid || undefined,
268
277
  }}
269
278
  >
270
279
  {asChild ? children : field}
@@ -26,7 +26,14 @@ export type SelectTriggerProps = {
26
26
  */
27
27
  asChild?: boolean;
28
28
  disabled?: boolean;
29
- label: string;
29
+ /**
30
+ * The trigger's accessible name.
31
+ *
32
+ * The trigger draws the chosen value, which is not a name — "Medium" says
33
+ * nothing about what is medium. Inside a `Field` the field's label supplies
34
+ * it; with neither the trigger announces nothing.
35
+ */
36
+ label?: string;
30
37
  /**
31
38
  * Utility classes APPENDED to the part's own — never substituted for them, so
32
39
  * a single layout class cannot strip the chrome.
@@ -22,6 +22,7 @@ import { clamp } from '../styles/clamp';
22
22
  import { BUTTON_SHADOW, mixColor, resolveButtonRamps } from '../button/shared';
23
23
  import { useAccessibleNameWarning } from '../hooks/use-accessible-name-warning';
24
24
  import type { RangeSliderProps, SliderProps } from './types';
25
+ import { useFieldMembership } from '../field/membership';
25
26
 
26
27
  function quantize(raw: number, min: number, max: number, step: number): number {
27
28
  if (step <= 0) return clamp(raw, min, max);
@@ -184,7 +185,7 @@ function SliderBase({
184
185
  min,
185
186
  max,
186
187
  step,
187
- disabled,
188
+ disabled: disabledProp,
188
189
  trackHeight,
189
190
  thumbSize,
190
191
  minimumTrackTintColor,
@@ -199,6 +200,12 @@ function SliderBase({
199
200
  testID,
200
201
  }: SliderBaseProps) {
201
202
  const theme = useTheme();
203
+ // A slider draws no text of its own — its `label` is rendered but hidden from
204
+ // assistive technology, which is why the name has to be a prop. Inside a
205
+ // `Field` the field's label is that name (STACKED placement), and the field's
206
+ // `disabled` constrains the slider whatever the caller passed.
207
+ const field = useFieldMembership({ accessibilityLabel, label, disabled: disabledProp });
208
+ const disabled = field.disabled;
202
209
  const ringOffset = useRingOffsetStyle();
203
210
  React.useEffect(() => {
204
211
  adoptStyleSheet(STYLE_ID, BLOOM_SLIDER_CSS);
@@ -365,11 +372,14 @@ function SliderBase({
365
372
  return (
366
373
  <View
367
374
  testID={testID}
375
+ nativeID={field.nativeID}
376
+ aria-describedby={field.describedBy}
377
+ aria-invalid={field.invalid || undefined}
368
378
  {...(isRange
369
- ? { accessibilityLabel: accessibilityLabel ?? label }
379
+ ? { accessibilityLabel: field.accessibilityLabel }
370
380
  : {
371
381
  accessibilityRole: 'adjustable' as const,
372
- accessibilityLabel: accessibilityLabel ?? label,
382
+ accessibilityLabel: field.accessibilityLabel,
373
383
  // react-native-web reads the FLAT `aria-value*` props; it has no
374
384
  // handling for the `accessibilityValue` object at all, so this
375
385
  // rendered a `role="slider"` carrying no value whatsoever. React
@@ -14,6 +14,7 @@ import { useTheme } from '../theme/use-theme';
14
14
  import { Text } from '../typography';
15
15
  import type { TypeScaleVariant } from '../typography/scale';
16
16
  import type { StepperProps, StepperSize } from './types';
17
+ import { useFieldMembership } from '../field/membership';
17
18
 
18
19
  /**
19
20
  * Bloom's numeric counter: a round `−` button, the value, a round `+` button.
@@ -77,7 +78,7 @@ function StepperComponent({
77
78
  min = 0,
78
79
  max,
79
80
  step = 1,
80
- disabled = false,
81
+ disabled: disabledProp = false,
81
82
  size = 'medium',
82
83
  formatValue,
83
84
  accessibilityLabel,
@@ -88,7 +89,12 @@ function StepperComponent({
88
89
  }: StepperProps) {
89
90
  const theme = useTheme();
90
91
  const ringOffset = useRingOffsetStyle();
91
- useAccessibleNameWarning('Stepper', accessibilityLabel);
92
+ // The digits are not a name, so the stepper is named by a prop or by the
93
+ // enclosing `Field` — and the warning has to see the resolved name, or a
94
+ // field-labelled stepper warns anyway.
95
+ const field = useFieldMembership({ accessibilityLabel, disabled: disabledProp });
96
+ const disabled = field.disabled;
97
+ useAccessibleNameWarning('Stepper', field.accessibilityLabel);
92
98
  useInteractiveWebCss(STYLE_ID, BLOOM_STEPPER_CSS);
93
99
  const { accent } = useMemo(() => resolveButtonRamps(theme), [theme]);
94
100
  const config = SIZE_CONFIG[size];
@@ -166,7 +172,9 @@ function StepperComponent({
166
172
  <View
167
173
  testID={testID}
168
174
  role="group"
169
- accessibilityLabel={accessibilityLabel}
175
+ nativeID={field.nativeID}
176
+ accessibilityLabel={field.accessibilityLabel}
177
+ aria-describedby={field.describedBy}
170
178
  aria-disabled={disabled || undefined}
171
179
  style={[{ flexDirection: 'row', alignItems: 'center', gap: config.gap }, style]}
172
180
  >
@@ -183,7 +191,7 @@ function StepperComponent({
183
191
  />
184
192
  <View
185
193
  accessibilityRole="adjustable"
186
- accessibilityLabel={accessibilityLabel}
194
+ accessibilityLabel={field.accessibilityLabel}
187
195
  aria-valuemin={min}
188
196
  aria-valuemax={max}
189
197
  aria-valuenow={value}
@@ -26,7 +26,14 @@ export interface StepperProps {
26
26
  * number and two glyphs, so nothing else can tell a screen reader which of
27
27
  * several counters this is.
28
28
  */
29
- accessibilityLabel: string;
29
+ /**
30
+ * The control's accessible name.
31
+ *
32
+ * Optional only because an enclosing `Field`'s label can supply it — with
33
+ * neither, the control announces nothing and warns once in development
34
+ * (`hooks/use-accessible-name-warning.ts`).
35
+ */
36
+ accessibilityLabel?: string;
30
37
  /** Name of the `−` button. Default `"Decrease"`. */
31
38
  decrementLabel?: string;
32
39
  /** Name of the `+` button. Default `"Increase"`. */
@@ -73,6 +73,22 @@ export interface WebAriaProps {
73
73
  * currently needs; the union is the ARIA one, narrowed to what is spellable.
74
74
  */
75
75
  'aria-haspopup'?: 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog';
76
+ /**
77
+ * The id list of the elements that DESCRIBE this control — a field's hint or
78
+ * its error message (`field/context.ts`).
79
+ *
80
+ * Here for the same reason as `aria-haspopup`: React Native types no such prop
81
+ * (there is no `accessibilityState` entry for it to fold into) while
82
+ * react-native-web forwards it, so web gets the association and native is
83
+ * unaffected. A control that needs the description READ on native carries it
84
+ * in its accessible name instead.
85
+ */
86
+ 'aria-describedby'?: string;
87
+ /**
88
+ * Whether the control's current value is invalid. Same platform split as
89
+ * `aria-describedby`.
90
+ */
91
+ 'aria-invalid'?: boolean;
76
92
  }
77
93
 
78
94
  /**
@@ -5,6 +5,7 @@ import { useTheme } from '../theme/use-theme';
5
5
  import { animation } from '../styles/tokens';
6
6
  import { bloomShadowStyle } from '../design-tokens/shadows';
7
7
  import { useAccessibleNameWarning } from '../hooks/use-accessible-name-warning';
8
+ import { useFieldMembership } from '../field/membership';
8
9
  import type { SwitchProps } from './types';
9
10
 
10
11
  const TRACK = { default: { w: 44, h: 26 }, sm: { w: 36, h: 22 } } as const;
@@ -13,9 +14,18 @@ const PADDING = 2;
13
14
  const SQUEEZE_RATIO = 0.75; // thumb height shrinks to 75% when pressed
14
15
 
15
16
  const SwitchComponent = React.forwardRef<React.ElementRef<typeof Pressable>, SwitchProps>(
16
- ({ value, onValueChange, disabled, style, size = 'default', accessibilityLabel, testID }, ref) => {
17
+ (
18
+ { value, onValueChange, disabled, style, size = 'default', accessibilityLabel, nativeID, testID },
19
+ ref,
20
+ ) => {
17
21
  const theme = useTheme();
18
- useAccessibleNameWarning('Switch', accessibilityLabel);
22
+ // A switch draws no text, so a `Field` is often the only thing that names
23
+ // it — and the warning has to see the RESOLVED name, or a correctly
24
+ // labelled switch inside a field warns anyway and the warning stops
25
+ // meaning anything.
26
+ const field = useFieldMembership({ accessibilityLabel, disabled, nativeID });
27
+ const isDisabled = field.disabled;
28
+ useAccessibleNameWarning('Switch', field.accessibilityLabel);
19
29
  const anim = useRef(new Animated.Value(value ? 1 : 0)).current;
20
30
  const pressAnim = useRef(new Animated.Value(0)).current;
21
31
 
@@ -32,18 +42,18 @@ const SwitchComponent = React.forwardRef<React.ElementRef<typeof Pressable>, Swi
32
42
  }
33
43
 
34
44
  const handlePress = useCallback(() => {
35
- if (disabled) return;
45
+ if (isDisabled) return;
36
46
  onValueChange(!value);
37
- }, [disabled, value, onValueChange]);
47
+ }, [isDisabled, value, onValueChange]);
38
48
 
39
49
  const onPressIn = useCallback(() => {
40
- if (disabled) return;
50
+ if (isDisabled) return;
41
51
  Animated.spring(pressAnim, {
42
52
  toValue: 1,
43
53
  useNativeDriver: false,
44
54
  ...animation.spring.snappy,
45
55
  }).start();
46
- }, [disabled, pressAnim]);
56
+ }, [isDisabled, pressAnim]);
47
57
 
48
58
  const onPressOut = useCallback(() => {
49
59
  Animated.spring(pressAnim, {
@@ -91,18 +101,21 @@ const SwitchComponent = React.forwardRef<React.ElementRef<typeof Pressable>, Swi
91
101
  // `createDOMProps` emits `aria-label` from it, React Native reads it
92
102
  // directly. That is NOT the rule for `accessibilityState` two lines up,
93
103
  // which reaches native alone; the state and the name differ here.
94
- accessibilityLabel={accessibilityLabel}
104
+ accessibilityLabel={field.accessibilityLabel}
105
+ nativeID={field.nativeID}
106
+ aria-describedby={field.describedBy}
107
+ aria-invalid={field.invalid || undefined}
95
108
  // The disabled state has to travel on this prop, not on `aria-disabled`:
96
109
  // react-native-web's `Pressable` appends its own `aria-disabled` from
97
110
  // `disabled` AFTER spreading the caller's props, so a caller-supplied
98
111
  // one is overwritten. `handlePress` already no-ops when disabled, so
99
112
  // this only adds what was missing — the announced state, and skipping
100
113
  // the control in the tab order.
101
- disabled={disabled}
114
+ disabled={isDisabled}
102
115
  onPress={handlePress}
103
116
  onPressIn={onPressIn}
104
117
  onPressOut={onPressOut}
105
- style={[disabled && styles.disabled, style]}
118
+ style={[isDisabled && styles.disabled, style]}
106
119
  hitSlop={HIT_SLOP}
107
120
  testID={testID}
108
121
  >
@@ -26,7 +26,14 @@ export interface SwitchProps {
26
26
  * are not the same rule.)
27
27
  *
28
28
  * Omitting it warns once in development; see `use-accessible-name-warning`.
29
+ * Inside a `Field` the field's label supplies the name, so a switch in a
30
+ * labelled field needs nothing here and does not warn.
29
31
  */
30
32
  accessibilityLabel?: string;
33
+ /**
34
+ * The switch's id. Inside a `Field` the field supplies one, so the label's
35
+ * `htmlFor` points at the control on web.
36
+ */
37
+ nativeID?: string;
31
38
  testID?: string;
32
39
  }