@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
@@ -24,6 +24,7 @@ import {
24
24
  } from '../text-field/shared';
25
25
  import { TYPE_SCALE } from '../typography/scale';
26
26
  import type { InputOtpProps } from './types';
27
+ import { useFieldMembership } from '../field/membership';
27
28
 
28
29
  /**
29
30
  * `InputOtp`: one box per digit, side by side, behaving as ONE value.
@@ -185,13 +186,25 @@ export function InputOtp({
185
186
  disabled: disabledAlias,
186
187
  groupEvery,
187
188
  autoFocus = false,
188
- accessibilityLabel = 'One-time code',
189
+ accessibilityLabel,
189
190
  style,
190
191
  testID,
191
192
  }: InputOtpProps) {
192
193
  const theme = useTheme();
193
194
  const palette = useMemo(() => resolveInputOtpPalette(theme), [theme]);
194
- const disabled = isDisabled ?? disabledAlias ?? false;
195
+ // SEVERAL controls behind one name: the boxes are the field's `multiple` case
196
+ // (`docs/field.mdx`), so the GROUP takes the field's name and description and
197
+ // each box keeps its own name and its own `aria-invalid`. A caller's own
198
+ // `accessibilityLabel` still wins; `'One-time code'` is the last resort rather
199
+ // than a default that would outrank the field's label.
200
+ const membership = useFieldMembership({
201
+ accessibilityLabel,
202
+ disabled: isDisabled ?? disabledAlias ?? false,
203
+ invalid: isInvalid,
204
+ });
205
+ const disabled = membership.disabled;
206
+ const invalid = membership.invalid;
207
+ const groupName = membership.accessibilityLabel ?? 'One-time code';
195
208
 
196
209
  const inputsRef = useRef<Array<TextInput | null>>([]);
197
210
  const [internal, setInternal] = useState(() => clean(defaultValue, length));
@@ -259,7 +272,8 @@ export function InputOtp({
259
272
  return (
260
273
  <View
261
274
  role="group"
262
- accessibilityLabel={accessibilityLabel}
275
+ accessibilityLabel={groupName}
276
+ aria-describedby={membership.describedBy}
263
277
  testID={testID}
264
278
  // No `aria-invalid` here: `group` does not support it. Each box carries
265
279
  // its own, which is where a screen reader looks anyway.
@@ -270,7 +284,7 @@ export function InputOtp({
270
284
  const paint = resolveInputOtpBoxPaint(palette, {
271
285
  hovered: hoveredIndex === index,
272
286
  focused: focusedIndex === index,
273
- invalid: isInvalid,
287
+ invalid,
274
288
  disabled,
275
289
  });
276
290
 
@@ -302,7 +316,7 @@ export function InputOtp({
302
316
  caretHidden={false}
303
317
  editable={!disabled}
304
318
  accessibilityLabel={`Digit ${index + 1} of ${length}`}
305
- aria-invalid={isInvalid || undefined}
319
+ aria-invalid={invalid || undefined}
306
320
  aria-disabled={disabled || undefined}
307
321
  value={digit === ' ' ? '' : digit}
308
322
  onChangeText={(text) => writeFrom(index, text)}
@@ -20,7 +20,13 @@ export interface InputOtpProps extends ViewStyleProp {
20
20
  groupEvery?: number;
21
21
  /** Focus the first box on mount. */
22
22
  autoFocus?: boolean;
23
- /** The group's accessible name, default `"One-time code"`. */
23
+ /**
24
+ * The group's accessible name.
25
+ *
26
+ * Inside a `Field` the field's label supplies it. `"One-time code"` is the
27
+ * last resort when neither is given — a default here would outrank the
28
+ * field's label, which is the one thing on screen the user can read.
29
+ */
24
30
  accessibilityLabel?: string;
25
31
  testID?: string;
26
32
  }
@@ -5,9 +5,12 @@
5
5
  * `edge` answers "how far from the edge"; `bottom-edge` and `top-edge` answer
6
6
  * "what is already parked there"; `scroll-offset` answers "which scroller am I
7
7
  * following". A surface that floats at an edge needs the first two; chrome that
8
- * reacts to scrolling needs the third.
8
+ * reacts to scrolling needs the third. `screen-scope` answers "am I my own
9
+ * screen" — a modal or a sheet, whose chrome must claim and read its own edges
10
+ * rather than the screen's underneath.
9
11
  */
10
12
  export { EDGE_GAP, windowEdgeGap } from './edge';
11
13
  export { BottomEdgeProvider, useBottomEdgeInset, useClaimBottomEdge } from './bottom-edge';
12
14
  export { TopEdgeProvider, useClaimTopEdge, useTopEdgeInset } from './top-edge';
13
15
  export { ScrollOffsetProvider, useScrollOffset } from './scroll-offset';
16
+ export { ScreenScope } from './screen-scope';
@@ -0,0 +1,54 @@
1
+ /**
2
+ * A surface that is its OWN screen, for the two contracts that are screen-scoped.
3
+ *
4
+ * ## What goes wrong without it
5
+ *
6
+ * `top-edge.tsx` and `scroll-offset.ts` are per-SCREEN by design: one registry
7
+ * of what is parked at the top, one scroller that the chrome follows. A modal,
8
+ * a bottom sheet or a detail panel is a second screen drawn over the first, and
9
+ * both contracts reach into it from outside:
10
+ *
11
+ * - A header inside a full-screen dialog CLAIMS the top edge of the screen
12
+ * underneath it. The claim combines with `max`, so the page behind the modal
13
+ * is padded for a header it cannot see — and when the modal closes the claim
14
+ * unregisters, so the padding appears and disappears with the modal.
15
+ * - Chrome inside the sheet READS the page's scroll offset and follows a
16
+ * scroller the user is not touching. The sheet's own list scrolls and the
17
+ * sheet's header does not react; the page behind scrolls and it does.
18
+ *
19
+ * Neither throws, neither warns, and both look like layout decisions.
20
+ *
21
+ * ## What it does
22
+ *
23
+ * Nests a fresh `TopEdgeProvider` (an empty registry the surface's own chrome
24
+ * claims against) and publishes a `null` scroll offset, which is the honest
25
+ * value: nothing inside has said which scroller it owns yet. A scroll owner
26
+ * INSIDE the surface publishes its own offset in the ordinary way and wins,
27
+ * because it is nearer.
28
+ *
29
+ * The BOTTOM edge is scoped too, and for the same reason read from the other
30
+ * side: the bottom edge is the tab bar and the FAB, and a sheet covers both. A
31
+ * form inside a sheet that padded itself for a tab bar it is drawn over ends in
32
+ * a band of empty space.
33
+ *
34
+ * Bloom's own overlay surfaces mount it (`DialogBody`, `BottomSheetBase`), so a
35
+ * consumer gets this by using them. It is exported for a surface Bloom does not
36
+ * own — an app's own full-screen route presented over another.
37
+ */
38
+ import type { PropsWithChildren } from 'react';
39
+
40
+ import { BottomEdgeProvider } from './bottom-edge';
41
+ import { ScrollOffsetProvider } from './scroll-offset';
42
+ import { TopEdgeProvider } from './top-edge';
43
+
44
+ export function ScreenScope({ children }: PropsWithChildren) {
45
+ return (
46
+ <TopEdgeProvider>
47
+ <BottomEdgeProvider>
48
+ {/* `null`, not a zero SharedValue: the two mean different things to the
49
+ chrome reading it (`scroll-offset.ts`). */}
50
+ <ScrollOffsetProvider value={null}>{children}</ScrollOffsetProvider>
51
+ </BottomEdgeProvider>
52
+ </TopEdgeProvider>
53
+ );
54
+ }
@@ -11,6 +11,7 @@ import {
11
11
  } from '../text-field';
12
12
  import { CountryCodeSelect } from './CountryCodeSelect';
13
13
  import type { PhoneInputProps } from './types';
14
+ import { useFieldMembership } from '../field/membership';
14
15
 
15
16
  /**
16
17
  * A phone number field: `Input` with a country-code `Select` in its
@@ -29,7 +30,7 @@ export function PhoneInput({
29
30
  hint,
30
31
  required,
31
32
  tooltip,
32
- size = 'medium',
33
+ size,
33
34
  isInvalid,
34
35
  disabled,
35
36
  value,
@@ -46,6 +47,17 @@ export function PhoneInput({
46
47
  testID,
47
48
  style,
48
49
  }: PhoneInputProps) {
50
+ // The country select is a SECOND control inside this field, and it is the one
51
+ // a `Field disabled` used to miss: the number input reads the field context
52
+ // itself, the select does not, so the picker stayed operable inside a disabled
53
+ // field. Resolving membership here hands the same state to both halves.
54
+ const field = useFieldMembership({
55
+ accessibilityLabel,
56
+ label: label ?? 'Phone number',
57
+ disabled,
58
+ invalid: isInvalid,
59
+ required,
60
+ });
49
61
  const [text, setText] = useControllableState({
50
62
  value,
51
63
  defaultValue,
@@ -55,14 +67,14 @@ export function PhoneInput({
55
67
  return (
56
68
  <View style={[{ width: '100%' }, style]} testID={testID}>
57
69
  {label ? (
58
- <TextFieldLabel required={required} tooltip={tooltip}>
70
+ <TextFieldLabel required={field.required} tooltip={tooltip}>
59
71
  {label}
60
72
  </TextFieldLabel>
61
73
  ) : null}
62
74
  <TextField
63
75
  size={size}
64
- isInvalid={isInvalid}
65
- disabled={disabled}
76
+ isInvalid={field.invalid}
77
+ disabled={field.disabled}
66
78
  leadingAddon={
67
79
  <CountryCodeSelect
68
80
  value={country}
@@ -70,15 +82,15 @@ export function PhoneInput({
70
82
  onValueChange={onCountryChange}
71
83
  countries={countries}
72
84
  label={countrySelectLabel}
73
- disabled={disabled}
85
+ disabled={field.disabled}
74
86
  />
75
87
  }>
76
88
  <TextFieldInput
77
- label={accessibilityLabel ?? label ?? 'Phone number'}
89
+ label={field.accessibilityLabel ?? 'Phone number'}
78
90
  placeholder={placeholder ?? null}
79
91
  value={text}
80
92
  onChangeText={setText}
81
- isInvalid={isInvalid}
93
+ isInvalid={field.invalid}
82
94
  keyboardType="phone-pad"
83
95
  autoComplete="tel"
84
96
  textContentType="telephoneNumber"
@@ -87,7 +99,7 @@ export function PhoneInput({
87
99
  />
88
100
  {trailingIcon ? <TextFieldIcon icon={trailingIcon} position="trailing" /> : null}
89
101
  </TextField>
90
- {hint ? <TextFieldHint isInvalid={isInvalid}>{hint}</TextFieldHint> : null}
102
+ {hint ? <TextFieldHint isInvalid={field.invalid}>{hint}</TextFieldHint> : null}
91
103
  </View>
92
104
  );
93
105
  }
@@ -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.