@oxy.so/bloom 3.0.1 → 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 (269) 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/input-group.mdx +22 -0
  8. package/docs/input-otp.mdx +14 -0
  9. package/docs/layout.mdx +34 -0
  10. package/docs/migrating-to-3.0.mdx +43 -4
  11. package/docs/phone-input.mdx +17 -0
  12. package/docs/radio.mdx +16 -0
  13. package/docs/rating.mdx +14 -0
  14. package/docs/segmented-control.mdx +16 -4
  15. package/docs/select.mdx +22 -0
  16. package/docs/slider.mdx +14 -0
  17. package/docs/stepper.mdx +3 -1
  18. package/docs/switch.mdx +14 -0
  19. package/docs/text-field.mdx +14 -0
  20. package/docs/textarea.mdx +20 -0
  21. package/lib/commonjs/bottom-sheet/BottomSheetBase.js +11 -4
  22. package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
  23. package/lib/commonjs/checkbox/Checkbox.js +24 -8
  24. package/lib/commonjs/checkbox/Checkbox.js.map +1 -1
  25. package/lib/commonjs/date-picker/TimeField.js +18 -6
  26. package/lib/commonjs/date-picker/TimeField.js.map +1 -1
  27. package/lib/commonjs/dialog/DialogContent.js +36 -29
  28. package/lib/commonjs/dialog/DialogContent.js.map +1 -1
  29. package/lib/commonjs/field/index.js +7 -0
  30. package/lib/commonjs/field/index.js.map +1 -1
  31. package/lib/commonjs/field/membership.js +105 -0
  32. package/lib/commonjs/field/membership.js.map +1 -0
  33. package/lib/commonjs/file-upload/FileUpload.js +17 -3
  34. package/lib/commonjs/file-upload/FileUpload.js.map +1 -1
  35. package/lib/commonjs/index.js +21 -0
  36. package/lib/commonjs/index.js.map +1 -1
  37. package/lib/commonjs/index.web.js +21 -0
  38. package/lib/commonjs/index.web.js.map +1 -1
  39. package/lib/commonjs/input-group/InputGroup.js +18 -5
  40. package/lib/commonjs/input-group/InputGroup.js.map +1 -1
  41. package/lib/commonjs/input-otp/InputOtp.js +20 -8
  42. package/lib/commonjs/input-otp/InputOtp.js.map +1 -1
  43. package/lib/commonjs/layout/index.js +7 -0
  44. package/lib/commonjs/layout/index.js.map +1 -1
  45. package/lib/commonjs/layout/screen-scope.js +61 -0
  46. package/lib/commonjs/layout/screen-scope.js.map +1 -0
  47. package/lib/commonjs/phone-input/PhoneInput.js +20 -8
  48. package/lib/commonjs/phone-input/PhoneInput.js.map +1 -1
  49. package/lib/commonjs/radio/Radio.js +35 -9
  50. package/lib/commonjs/radio/Radio.js.map +1 -1
  51. package/lib/commonjs/rating/RatingInput.js +14 -6
  52. package/lib/commonjs/rating/RatingInput.js.map +1 -1
  53. package/lib/commonjs/segmented-control/SegmentedControl.js +38 -8
  54. package/lib/commonjs/segmented-control/SegmentedControl.js.map +1 -1
  55. package/lib/commonjs/select/Select.js +15 -3
  56. package/lib/commonjs/select/Select.js.map +1 -1
  57. package/lib/commonjs/select/Select.web.js +13 -5
  58. package/lib/commonjs/select/Select.web.js.map +1 -1
  59. package/lib/commonjs/slider/Slider.js +17 -3
  60. package/lib/commonjs/slider/Slider.js.map +1 -1
  61. package/lib/commonjs/stepper/Stepper.js +16 -7
  62. package/lib/commonjs/stepper/Stepper.js.map +1 -1
  63. package/lib/commonjs/styles/styled-primitives.js.map +1 -1
  64. package/lib/commonjs/switch/Switch.js +23 -8
  65. package/lib/commonjs/switch/Switch.js.map +1 -1
  66. package/lib/commonjs/text-field/TextField.js +19 -4
  67. package/lib/commonjs/text-field/TextField.js.map +1 -1
  68. package/lib/commonjs/textarea/Textarea.js +39 -15
  69. package/lib/commonjs/textarea/Textarea.js.map +1 -1
  70. package/lib/module/bottom-sheet/BottomSheetBase.js +8 -1
  71. package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
  72. package/lib/module/checkbox/Checkbox.js +24 -8
  73. package/lib/module/checkbox/Checkbox.js.map +1 -1
  74. package/lib/module/date-picker/TimeField.js +19 -5
  75. package/lib/module/date-picker/TimeField.js.map +1 -1
  76. package/lib/module/dialog/DialogContent.js +37 -30
  77. package/lib/module/dialog/DialogContent.js.map +1 -1
  78. package/lib/module/field/index.js +1 -0
  79. package/lib/module/field/index.js.map +1 -1
  80. package/lib/module/field/membership.js +101 -0
  81. package/lib/module/field/membership.js.map +1 -0
  82. package/lib/module/file-upload/FileUpload.js +17 -3
  83. package/lib/module/file-upload/FileUpload.js.map +1 -1
  84. package/lib/module/index.js +2 -2
  85. package/lib/module/index.js.map +1 -1
  86. package/lib/module/index.web.js +2 -2
  87. package/lib/module/index.web.js.map +1 -1
  88. package/lib/module/input-group/InputGroup.js +19 -4
  89. package/lib/module/input-group/InputGroup.js.map +1 -1
  90. package/lib/module/input-otp/InputOtp.js +21 -7
  91. package/lib/module/input-otp/InputOtp.js.map +1 -1
  92. package/lib/module/layout/index.js +4 -1
  93. package/lib/module/layout/index.js.map +1 -1
  94. package/lib/module/layout/screen-scope.js +57 -0
  95. package/lib/module/layout/screen-scope.js.map +1 -0
  96. package/lib/module/phone-input/PhoneInput.js +22 -9
  97. package/lib/module/phone-input/PhoneInput.js.map +1 -1
  98. package/lib/module/radio/Radio.js +35 -9
  99. package/lib/module/radio/Radio.js.map +1 -1
  100. package/lib/module/rating/RatingInput.js +15 -5
  101. package/lib/module/rating/RatingInput.js.map +1 -1
  102. package/lib/module/segmented-control/SegmentedControl.js +38 -8
  103. package/lib/module/segmented-control/SegmentedControl.js.map +1 -1
  104. package/lib/module/select/Select.js +15 -3
  105. package/lib/module/select/Select.js.map +1 -1
  106. package/lib/module/select/Select.web.js +14 -4
  107. package/lib/module/select/Select.web.js.map +1 -1
  108. package/lib/module/slider/Slider.js +17 -3
  109. package/lib/module/slider/Slider.js.map +1 -1
  110. package/lib/module/stepper/Stepper.js +17 -6
  111. package/lib/module/stepper/Stepper.js.map +1 -1
  112. package/lib/module/styles/styled-primitives.js.map +1 -1
  113. package/lib/module/switch/Switch.js +23 -8
  114. package/lib/module/switch/Switch.js.map +1 -1
  115. package/lib/module/text-field/TextField.js +19 -4
  116. package/lib/module/text-field/TextField.js.map +1 -1
  117. package/lib/module/textarea/Textarea.js +39 -15
  118. package/lib/module/textarea/Textarea.js.map +1 -1
  119. package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  120. package/lib/typescript/commonjs/checkbox/Checkbox.d.ts.map +1 -1
  121. package/lib/typescript/commonjs/checkbox/types.d.ts +5 -0
  122. package/lib/typescript/commonjs/checkbox/types.d.ts.map +1 -1
  123. package/lib/typescript/commonjs/date-picker/TimeField.d.ts +1 -1
  124. package/lib/typescript/commonjs/date-picker/TimeField.d.ts.map +1 -1
  125. package/lib/typescript/commonjs/date-picker/types.d.ts +6 -3
  126. package/lib/typescript/commonjs/date-picker/types.d.ts.map +1 -1
  127. package/lib/typescript/commonjs/dialog/DialogContent.d.ts.map +1 -1
  128. package/lib/typescript/commonjs/field/index.d.ts +2 -0
  129. package/lib/typescript/commonjs/field/index.d.ts.map +1 -1
  130. package/lib/typescript/commonjs/field/membership.d.ts +60 -0
  131. package/lib/typescript/commonjs/field/membership.d.ts.map +1 -0
  132. package/lib/typescript/commonjs/file-upload/FileUpload.d.ts +1 -1
  133. package/lib/typescript/commonjs/file-upload/FileUpload.d.ts.map +1 -1
  134. package/lib/typescript/commonjs/floating/types.d.ts +13 -0
  135. package/lib/typescript/commonjs/floating/types.d.ts.map +1 -1
  136. package/lib/typescript/commonjs/index.d.ts +3 -3
  137. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  138. package/lib/typescript/commonjs/index.web.d.ts +3 -3
  139. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  140. package/lib/typescript/commonjs/input-group/InputGroup.d.ts +1 -1
  141. package/lib/typescript/commonjs/input-group/InputGroup.d.ts.map +1 -1
  142. package/lib/typescript/commonjs/input-otp/InputOtp.d.ts.map +1 -1
  143. package/lib/typescript/commonjs/input-otp/types.d.ts +7 -1
  144. package/lib/typescript/commonjs/input-otp/types.d.ts.map +1 -1
  145. package/lib/typescript/commonjs/layout/index.d.ts +4 -1
  146. package/lib/typescript/commonjs/layout/index.d.ts.map +1 -1
  147. package/lib/typescript/commonjs/layout/screen-scope.d.ts +40 -0
  148. package/lib/typescript/commonjs/layout/screen-scope.d.ts.map +1 -0
  149. package/lib/typescript/commonjs/phone-input/PhoneInput.d.ts.map +1 -1
  150. package/lib/typescript/commonjs/radio/Radio.d.ts +1 -1
  151. package/lib/typescript/commonjs/radio/Radio.d.ts.map +1 -1
  152. package/lib/typescript/commonjs/radio/types.d.ts +11 -3
  153. package/lib/typescript/commonjs/radio/types.d.ts.map +1 -1
  154. package/lib/typescript/commonjs/rating/RatingInput.d.ts +1 -1
  155. package/lib/typescript/commonjs/rating/RatingInput.d.ts.map +1 -1
  156. package/lib/typescript/commonjs/rating/types.d.ts +6 -3
  157. package/lib/typescript/commonjs/rating/types.d.ts.map +1 -1
  158. package/lib/typescript/commonjs/segmented-control/SegmentedControl.d.ts +18 -2
  159. package/lib/typescript/commonjs/segmented-control/SegmentedControl.d.ts.map +1 -1
  160. package/lib/typescript/commonjs/select/Select.d.ts.map +1 -1
  161. package/lib/typescript/commonjs/select/Select.web.d.ts.map +1 -1
  162. package/lib/typescript/commonjs/select/types.d.ts +8 -1
  163. package/lib/typescript/commonjs/select/types.d.ts.map +1 -1
  164. package/lib/typescript/commonjs/slider/Slider.d.ts.map +1 -1
  165. package/lib/typescript/commonjs/stepper/Stepper.d.ts +1 -1
  166. package/lib/typescript/commonjs/stepper/Stepper.d.ts.map +1 -1
  167. package/lib/typescript/commonjs/stepper/types.d.ts +8 -1
  168. package/lib/typescript/commonjs/stepper/types.d.ts.map +1 -1
  169. package/lib/typescript/commonjs/styles/styled-primitives.d.ts +16 -0
  170. package/lib/typescript/commonjs/styles/styled-primitives.d.ts.map +1 -1
  171. package/lib/typescript/commonjs/switch/Switch.d.ts.map +1 -1
  172. package/lib/typescript/commonjs/switch/types.d.ts +7 -0
  173. package/lib/typescript/commonjs/switch/types.d.ts.map +1 -1
  174. package/lib/typescript/commonjs/text-field/TextField.d.ts.map +1 -1
  175. package/lib/typescript/commonjs/textarea/Textarea.d.ts +1 -1
  176. package/lib/typescript/commonjs/textarea/Textarea.d.ts.map +1 -1
  177. package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  178. package/lib/typescript/module/checkbox/Checkbox.d.ts.map +1 -1
  179. package/lib/typescript/module/checkbox/types.d.ts +5 -0
  180. package/lib/typescript/module/checkbox/types.d.ts.map +1 -1
  181. package/lib/typescript/module/date-picker/TimeField.d.ts +1 -1
  182. package/lib/typescript/module/date-picker/TimeField.d.ts.map +1 -1
  183. package/lib/typescript/module/date-picker/types.d.ts +6 -3
  184. package/lib/typescript/module/date-picker/types.d.ts.map +1 -1
  185. package/lib/typescript/module/dialog/DialogContent.d.ts.map +1 -1
  186. package/lib/typescript/module/field/index.d.ts +2 -0
  187. package/lib/typescript/module/field/index.d.ts.map +1 -1
  188. package/lib/typescript/module/field/membership.d.ts +60 -0
  189. package/lib/typescript/module/field/membership.d.ts.map +1 -0
  190. package/lib/typescript/module/file-upload/FileUpload.d.ts +1 -1
  191. package/lib/typescript/module/file-upload/FileUpload.d.ts.map +1 -1
  192. package/lib/typescript/module/floating/types.d.ts +13 -0
  193. package/lib/typescript/module/floating/types.d.ts.map +1 -1
  194. package/lib/typescript/module/index.d.ts +3 -3
  195. package/lib/typescript/module/index.d.ts.map +1 -1
  196. package/lib/typescript/module/index.web.d.ts +3 -3
  197. package/lib/typescript/module/index.web.d.ts.map +1 -1
  198. package/lib/typescript/module/input-group/InputGroup.d.ts +1 -1
  199. package/lib/typescript/module/input-group/InputGroup.d.ts.map +1 -1
  200. package/lib/typescript/module/input-otp/InputOtp.d.ts.map +1 -1
  201. package/lib/typescript/module/input-otp/types.d.ts +7 -1
  202. package/lib/typescript/module/input-otp/types.d.ts.map +1 -1
  203. package/lib/typescript/module/layout/index.d.ts +4 -1
  204. package/lib/typescript/module/layout/index.d.ts.map +1 -1
  205. package/lib/typescript/module/layout/screen-scope.d.ts +40 -0
  206. package/lib/typescript/module/layout/screen-scope.d.ts.map +1 -0
  207. package/lib/typescript/module/phone-input/PhoneInput.d.ts.map +1 -1
  208. package/lib/typescript/module/radio/Radio.d.ts +1 -1
  209. package/lib/typescript/module/radio/Radio.d.ts.map +1 -1
  210. package/lib/typescript/module/radio/types.d.ts +11 -3
  211. package/lib/typescript/module/radio/types.d.ts.map +1 -1
  212. package/lib/typescript/module/rating/RatingInput.d.ts +1 -1
  213. package/lib/typescript/module/rating/RatingInput.d.ts.map +1 -1
  214. package/lib/typescript/module/rating/types.d.ts +6 -3
  215. package/lib/typescript/module/rating/types.d.ts.map +1 -1
  216. package/lib/typescript/module/segmented-control/SegmentedControl.d.ts +18 -2
  217. package/lib/typescript/module/segmented-control/SegmentedControl.d.ts.map +1 -1
  218. package/lib/typescript/module/select/Select.d.ts.map +1 -1
  219. package/lib/typescript/module/select/Select.web.d.ts.map +1 -1
  220. package/lib/typescript/module/select/types.d.ts +8 -1
  221. package/lib/typescript/module/select/types.d.ts.map +1 -1
  222. package/lib/typescript/module/slider/Slider.d.ts.map +1 -1
  223. package/lib/typescript/module/stepper/Stepper.d.ts +1 -1
  224. package/lib/typescript/module/stepper/Stepper.d.ts.map +1 -1
  225. package/lib/typescript/module/stepper/types.d.ts +8 -1
  226. package/lib/typescript/module/stepper/types.d.ts.map +1 -1
  227. package/lib/typescript/module/styles/styled-primitives.d.ts +16 -0
  228. package/lib/typescript/module/styles/styled-primitives.d.ts.map +1 -1
  229. package/lib/typescript/module/switch/Switch.d.ts.map +1 -1
  230. package/lib/typescript/module/switch/types.d.ts +7 -0
  231. package/lib/typescript/module/switch/types.d.ts.map +1 -1
  232. package/lib/typescript/module/text-field/TextField.d.ts.map +1 -1
  233. package/lib/typescript/module/textarea/Textarea.d.ts +1 -1
  234. package/lib/typescript/module/textarea/Textarea.d.ts.map +1 -1
  235. package/package.json +2 -1
  236. package/src/bottom-sheet/BottomSheetBase.tsx +6 -1
  237. package/src/checkbox/Checkbox.tsx +24 -8
  238. package/src/checkbox/types.ts +5 -0
  239. package/src/date-picker/TimeField.tsx +14 -3
  240. package/src/date-picker/types.ts +6 -3
  241. package/src/dialog/DialogContent.tsx +6 -2
  242. package/src/field/index.ts +2 -0
  243. package/src/field/membership.ts +164 -0
  244. package/src/file-upload/FileUpload.tsx +14 -3
  245. package/src/floating/types.ts +13 -0
  246. package/src/index.ts +9 -2
  247. package/src/index.web.ts +9 -2
  248. package/src/input-group/InputGroup.tsx +13 -2
  249. package/src/input-otp/InputOtp.tsx +19 -5
  250. package/src/input-otp/types.ts +7 -1
  251. package/src/layout/index.ts +4 -1
  252. package/src/layout/screen-scope.tsx +54 -0
  253. package/src/phone-input/PhoneInput.tsx +20 -8
  254. package/src/radio/Radio.tsx +32 -9
  255. package/src/radio/types.ts +11 -3
  256. package/src/rating/RatingInput.tsx +10 -3
  257. package/src/rating/types.ts +6 -3
  258. package/src/segmented-control/SegmentedControl.tsx +51 -8
  259. package/src/select/Select.tsx +14 -2
  260. package/src/select/Select.web.tsx +11 -2
  261. package/src/select/types.ts +8 -1
  262. package/src/slider/Slider.tsx +13 -3
  263. package/src/stepper/Stepper.tsx +12 -4
  264. package/src/stepper/types.ts +8 -1
  265. package/src/styles/styled-primitives.ts +16 -0
  266. package/src/switch/Switch.tsx +22 -9
  267. package/src/switch/types.ts +7 -0
  268. package/src/text-field/TextField.tsx +16 -4
  269. package/src/textarea/Textarea.tsx +38 -15
@@ -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
  }
@@ -53,6 +53,8 @@ import type {
53
53
  TextFieldLabelProps,
54
54
  TextFieldProps,
55
55
  } from './types';
56
+ import { useInheritedControl } from '../control-surface';
57
+ import { useFieldMembership } from '../field/membership';
56
58
 
57
59
  interface TextFieldContextValue {
58
60
  inputRef: React.RefObject<TextInput | null>;
@@ -153,7 +155,7 @@ export function TextField({
153
155
  children,
154
156
  isInvalid: isInvalidProp = false,
155
157
  disabled: disabledProp = false,
156
- size: sizeProp = 'medium',
158
+ size: sizeProp,
157
159
  radius = TEXT_FIELD_RADIUS,
158
160
  leadingAddon,
159
161
  style,
@@ -161,9 +163,19 @@ export function TextField({
161
163
  const theme = useTheme();
162
164
  const group = useContext(TextFieldGroupContext);
163
165
  const bare = group !== null;
164
- const isInvalid = isInvalidProp || (group?.isInvalid ?? false);
165
- const disabled = disabledProp || (group?.disabled ?? false);
166
- const size = group?.size ?? sizeProp;
166
+ // The enclosing `Field`, read HERE as well as in `TextFieldInput`: the shell
167
+ // is what paints the disabled and invalid chrome, and a shell that waited for
168
+ // its input to report up would paint nothing at all around a child that is not
169
+ // Bloom's input. Both directions are OR, so the two readings cannot disagree.
170
+ const member = useFieldMembership({ disabled: disabledProp, invalid: isInvalidProp });
171
+ const isInvalid = member.invalid || (group?.isInvalid ?? false);
172
+ const disabled = member.disabled || (group?.disabled ?? false);
173
+ // An `InputGroup` outranks everything: its members have to be one height. Then
174
+ // the caller's own prop, then the container's density (`ControlSurface`), then
175
+ // `medium` — the precedence rule in `docs/composition.mdx`, with the group's
176
+ // geometry first because it is a constraint rather than a default.
177
+ const inheritedSize = useInheritedControl('density', sizeProp, 'medium');
178
+ const size = group?.size ?? inheritedSize;
167
179
  const inputRef = useRef<TextInput>(null);
168
180
  const [inputDisabled, setInputDisabled] = useState(false);
169
181
  const {
@@ -19,6 +19,8 @@ import {
19
19
  resolveShellPaint,
20
20
  useTextFieldPalette,
21
21
  } from '../text-field/shared';
22
+ import { useInheritedControl } from '../control-surface';
23
+ import { useFieldMembership } from '../field/membership';
22
24
  import type { TextareaProps } from './types';
23
25
 
24
26
  const IS_WEB = Platform.OS === 'web';
@@ -52,7 +54,7 @@ export function Textarea({
52
54
  onChangeText,
53
55
  onFocus,
54
56
  onBlur,
55
- size = 'medium',
57
+ size: sizeProp,
56
58
  rows = 3,
57
59
  autoResize = false,
58
60
  maxRows,
@@ -67,10 +69,29 @@ export function Textarea({
67
69
  inputStyle,
68
70
  inputRef,
69
71
  accessibilityLabel,
72
+ nativeID,
70
73
  testID,
71
74
  ...rest
72
75
  }: TextareaProps) {
73
76
  const theme = useTheme();
77
+ // The two contracts a textarea sits inside: a container's density
78
+ // (`ControlSurface`) and an enclosing `Field`'s association. The label is
79
+ // STACKED here, as it is on `TextField`, so the field's label wins — they are
80
+ // two spellings of one thing and rendering both is the mistake `docs/field.mdx`
81
+ // names.
82
+ const size = useInheritedControl('density', sizeProp, 'medium');
83
+ const field = useFieldMembership({
84
+ accessibilityLabel,
85
+ label: label ?? placeholder,
86
+ disabled,
87
+ invalid: isInvalid,
88
+ required,
89
+ nativeID,
90
+ // An `aria-describedby` the caller spread through `rest` is KEPT and the
91
+ // field's is added to it, the same way `TextFieldInput` treats it: the two
92
+ // are an id list, not a choice.
93
+ describedBy: (rest as Record<string, unknown>)['aria-describedby'] as string | undefined,
94
+ });
74
95
  const palette = useTextFieldPalette();
75
96
  const innerRef = useRef<TextInput>(null);
76
97
  const { state: hovered, onIn: onHoverIn, onOut: onHoverOut } = useInteractionState();
@@ -82,7 +103,7 @@ export function Textarea({
82
103
  const [typedLength, setTypedLength] = useState(() => (defaultValue ?? '').length);
83
104
  const count = value !== undefined ? value.length : typedLength;
84
105
 
85
- const state = { hovered, focused, invalid: isInvalid, disabled };
106
+ const state = { hovered, focused, invalid: field.invalid, disabled: field.disabled };
86
107
  const paint = resolveShellPaint(palette, state);
87
108
  const line = TEXT_FIELD_TEXT.lineHeight;
88
109
 
@@ -92,7 +113,7 @@ export function Textarea({
92
113
  fontSize: TEXT_FIELD_TEXT.fontSize,
93
114
  lineHeight: line,
94
115
  fontWeight: TEXT_FIELD_TEXT.fontWeight,
95
- color: disabled ? palette.textDisabled : palette.text,
116
+ color: field.disabled ? palette.textDisabled : palette.text,
96
117
  paddingLeft: TEXT_FIELD_INPUT_INSET,
97
118
  paddingRight: TEXT_FIELD_INPUT_INSET,
98
119
  paddingTop: 0,
@@ -114,18 +135,16 @@ export function Textarea({
114
135
  outlineStyle: 'none',
115
136
  resize: autoResize ? 'none' : resize,
116
137
  fieldSizing: autoResize ? 'content' : undefined,
117
- cursor: disabled ? 'not-allowed' : undefined,
138
+ cursor: field.disabled ? 'not-allowed' : undefined,
118
139
  } as unknown as TextStyle)
119
140
  : undefined,
120
141
  inputStyle,
121
142
  ]) as TextStyle;
122
143
 
123
- const name = label ?? accessibilityLabel ?? placeholder;
124
-
125
144
  return (
126
145
  <View style={[{ width: '100%' }, style]} testID={testID}>
127
146
  {label ? (
128
- <TextFieldLabel required={required} tooltip={tooltip}>
147
+ <TextFieldLabel required={field.required} tooltip={tooltip}>
129
148
  {label}
130
149
  </TextFieldLabel>
131
150
  ) : null}
@@ -143,24 +162,28 @@ export function Textarea({
143
162
  ]}
144
163
  {...(IS_WEB
145
164
  ? ({
146
- onClick: () => innerRef.current?.focus(),
165
+ onClick: () => {
166
+ if (!field.disabled) innerRef.current?.focus();
167
+ },
147
168
  onMouseEnter: onHoverIn,
148
169
  onMouseLeave: onHoverOut,
149
170
  } as Record<string, unknown>)
150
171
  : {})}>
151
172
  <TextInput
152
173
  {...rest}
153
- {...(IS_WEB && disabled ? ({ disabled: true } as Record<string, unknown>) : {})}
174
+ {...(IS_WEB && field.disabled ? ({ disabled: true } as Record<string, unknown>) : {})}
154
175
  ref={mergeRefs([innerRef, inputRef])}
155
176
  multiline
156
177
  numberOfLines={rows}
157
178
  value={value}
158
179
  defaultValue={defaultValue}
159
180
  maxLength={maxLength}
160
- editable={disabled ? false : rest.editable}
161
- accessibilityLabel={name}
162
- aria-invalid={isInvalid || undefined}
163
- aria-disabled={disabled || undefined}
181
+ editable={field.disabled ? false : rest.editable}
182
+ nativeID={field.nativeID}
183
+ accessibilityLabel={field.accessibilityLabel}
184
+ aria-describedby={field.describedBy}
185
+ aria-invalid={field.invalid || undefined}
186
+ aria-disabled={field.disabled || undefined}
164
187
  placeholder={placeholder}
165
188
  placeholderTextColor={resolvePlaceholderColor(palette, state)}
166
189
  keyboardAppearance={theme.isDark ? 'dark' : 'light'}
@@ -190,7 +213,7 @@ export function Textarea({
190
213
  marginTop: TEXT_FIELD_STACK_GAP,
191
214
  }}>
192
215
  {hint ? (
193
- <TextFieldHint isInvalid={isInvalid} style={{ marginTop: 0 }}>
216
+ <TextFieldHint isInvalid={field.invalid} style={{ marginTop: 0 }}>
194
217
  {hint}
195
218
  </TextFieldHint>
196
219
  ) : null}
@@ -202,7 +225,7 @@ export function Textarea({
202
225
  flexShrink: 0,
203
226
  paddingTop: 1,
204
227
  fontVariant: ['tabular-nums'],
205
- color: isInvalid ? palette.error : palette.count,
228
+ color: field.invalid ? palette.error : palette.count,
206
229
  }}>
207
230
  {maxLength ? `${count}/${maxLength}` : String(count)}
208
231
  </Text>