@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
@@ -9,6 +9,8 @@ import { TYPE_SCALE } from '../typography';
9
9
  import { resolveCalendarPalette } from './palette';
10
10
  import { formatTime, parseTime, snapTime, stepTime } from './time';
11
11
  import type { TimeFieldProps, TimeFieldSize } from './types';
12
+ import { useInheritedControl } from '../control-surface';
13
+ import { useFieldMembership } from '../field/membership';
12
14
 
13
15
  /**
14
16
  * A time of day, typed. `DatePicker` is the day; this is the hour and minute,
@@ -60,8 +62,8 @@ function TimeFieldComponent({
60
62
  max,
61
63
  step = 1,
62
64
  hourFormat = '24h',
63
- size = 'medium',
64
- disabled = false,
65
+ size: sizeProp,
66
+ disabled: disabledProp = false,
65
67
  accessibilityLabel,
66
68
  placeholder,
67
69
  width,
@@ -69,6 +71,12 @@ function TimeFieldComponent({
69
71
  testID,
70
72
  }: TimeFieldProps) {
71
73
  const theme = useTheme();
74
+ // `"--:--"` names nothing, so the name is a prop — or, inside a `Field`, the
75
+ // field's label. The density contract supplies the size the same way: a
76
+ // container can ask for `small` once instead of on every control.
77
+ const size = useInheritedControl('density', sizeProp, 'medium');
78
+ const field = useFieldMembership({ accessibilityLabel, disabled: disabledProp });
79
+ const disabled = field.disabled;
72
80
  const palette = useMemo(() => resolveCalendarPalette(theme), [theme]);
73
81
  const config = SIZE_CONFIG[size];
74
82
 
@@ -150,7 +158,10 @@ function TimeFieldComponent({
150
158
  placeholder={placeholder ?? (hourFormat === '12h' ? '--:-- --' : '--:--')}
151
159
  placeholderTextColor={palette.secondaryText}
152
160
  inputMode={hourFormat === '12h' ? 'text' : 'numeric'}
153
- accessibilityLabel={accessibilityLabel}
161
+ nativeID={field.nativeID}
162
+ accessibilityLabel={field.accessibilityLabel}
163
+ aria-describedby={field.describedBy}
164
+ aria-invalid={field.invalid || undefined}
154
165
  aria-disabled={disabled || undefined}
155
166
  style={[FIELD_TRANSITION, fieldStyle, style]}
156
167
  testID={testID}
@@ -207,10 +207,13 @@ export interface TimeFieldProps {
207
207
  size?: TimeFieldSize;
208
208
  disabled?: boolean;
209
209
  /**
210
- * The NAME of the time ("Viewing time", "Check-in"). Required: the field
211
- * draws no label of its own, and `"--:--"` names nothing.
210
+ * The NAME of the time ("Viewing time", "Check-in").
211
+ *
212
+ * The field draws no label of its own and `"--:--"` names nothing, so this or
213
+ * an enclosing `Field`'s label is the only route to a name. With neither it
214
+ * warns once in development (`hooks/use-accessible-name-warning.ts`).
212
215
  */
213
- accessibilityLabel: string;
216
+ accessibilityLabel?: string;
214
217
  /** The text while empty. Default `"--:--"`, or `"--:-- --"` in 12h. */
215
218
  placeholder?: string;
216
219
  /** Fixed width. Default: 104 at `medium`, 96 at `small`. */
@@ -4,6 +4,7 @@ import { View, type GestureResponderEvent } from 'react-native';
4
4
  import { Text } from '../typography';
5
5
 
6
6
  import { useTheme } from '../theme/use-theme';
7
+ import { ScreenScope } from '../layout';
7
8
  import { Button } from '../button';
8
9
  import type { ButtonVariant } from '../button/types';
9
10
  import { useDialogContext } from './context';
@@ -40,7 +41,10 @@ export function DialogBody({
40
41
  }) {
41
42
  const theme = useTheme();
42
43
  return (
43
- <>
44
+ // A dialog is its own screen: chrome inside it claims THIS surface's top
45
+ // edge and follows THIS surface's scroller, not the page's underneath
46
+ // (`layout/screen-scope.tsx`).
47
+ <ScreenScope>
44
48
  {title ? (
45
49
  // Bloom's `Text`, not react-native's: the raw one renders in whatever
46
50
  // font the platform picks and takes no theme colour, so every dialog
@@ -74,7 +78,7 @@ export function DialogBody({
74
78
  ) : null}
75
79
  {children}
76
80
  {actions && actions.length > 0 ? <ActionRow actions={actions} /> : null}
77
- </>
81
+ </ScreenScope>
78
82
  );
79
83
  }
80
84
 
@@ -1,5 +1,7 @@
1
1
  export { Field } from './Field';
2
2
  export { useFieldControl } from './context';
3
+ export { useFieldMembership } from './membership';
3
4
 
4
5
  export type { FieldControlValue } from './context';
6
+ export type { FieldLabelPlacement, FieldMembership, FieldMembershipInput } from './membership';
5
7
  export type { FieldProps } from './types';
@@ -0,0 +1,164 @@
1
+ /**
2
+ * ONE resolution of field membership, for every control that opts in.
3
+ *
4
+ * `useFieldControl()` hands a control the field's ids and state; applying them
5
+ * is four separate decisions, and each has a direction that is silently wrong:
6
+ *
7
+ * the NAME defers to the field (`??`) — a control inside a labelled
8
+ * field must not announce nothing
9
+ * the CONSTRAINTS combine with the field (`||`) — `disabled` and `invalid`
10
+ * restrict, so a control cannot opt out
11
+ * the ID is the field's (`??` the caller's own, which wins)
12
+ * `describedBy` is a LIST, so the two are joined rather than one chosen
13
+ *
14
+ * Fifteen families resolving that by hand is fifteen chances to write `??` where
15
+ * `||` belongs — and the failure is silent in both directions: a control that
16
+ * re-enables itself inside a disabled field looks fine and a field-labelled
17
+ * control that announces nothing looks fine too. So it is a function, for the
18
+ * same reason `useInheritedControl` is one.
19
+ *
20
+ * `TextFieldInput` is the one family that resolves the same rule inline, because
21
+ * it also reconciles the text-field shell's own context and its standalone
22
+ * branch. It is not exempt from the RULE: `FieldMembership.test.tsx` asserts the
23
+ * observable properties for every family, so the two implementations cannot
24
+ * disagree about what a field means without going red.
25
+ *
26
+ * ## The name, and why placement decides it
27
+ *
28
+ * A control's own label is either STACKED above it (`TextField`, `Textarea`:
29
+ * the field's label and the control's label are two spellings of one thing, and
30
+ * a `Field` around it should win) or ADJACENT to it (a checkbox's caption, a
31
+ * radio's option text: the words the user reads AS the control, which the
32
+ * announced name must match). Getting that backwards makes a screen reader say
33
+ * one thing while the eye reads another, so it is a declared property of the
34
+ * family rather than a guess this hook could make.
35
+ *
36
+ * A control with neither — `Switch`, `Slider`, a progress bar — can be named by
37
+ * NOTHING but the field, which is the case this hook exists for.
38
+ */
39
+ import { useMemo } from 'react';
40
+
41
+ import { useFieldControl } from './context';
42
+
43
+ /** Where the control renders its own label, which decides which name wins. */
44
+ export type FieldLabelPlacement = 'stacked' | 'adjacent';
45
+
46
+ export interface FieldMembershipInput {
47
+ /** The name the caller wrote on the control. Always wins. */
48
+ accessibilityLabel?: string;
49
+ /** The control's own label, when it has one. */
50
+ label?: string;
51
+ /**
52
+ * Whether that label sits above the control (`stacked`, the default — the
53
+ * field's label is the same thing said twice, so the field wins) or beside it
54
+ * as the control's visible text (`adjacent` — it wins, so the announced name
55
+ * and the words on screen cannot disagree).
56
+ */
57
+ labelPlacement?: FieldLabelPlacement;
58
+ /** The control's own `disabled`. Combined with the field's, never replaced. */
59
+ disabled?: boolean;
60
+ /** The control's own invalid state. Combined with the field's. */
61
+ invalid?: boolean;
62
+ /** The control's own `required`. Combined with the field's. */
63
+ required?: boolean;
64
+ /** An id the caller wrote on the control. Outranks the field's. */
65
+ nativeID?: string;
66
+ /** An `aria-describedby` the caller wrote. JOINED with the field's, not replaced. */
67
+ describedBy?: string;
68
+ }
69
+
70
+ export interface FieldMembership {
71
+ /** Whether there is an enclosing `Field` at all. */
72
+ inField: boolean;
73
+ /** The id to render, or `undefined` when neither side has one. */
74
+ nativeID?: string;
75
+ /** The accessible name to render, or `undefined` when nothing names it. */
76
+ accessibilityLabel?: string;
77
+ /** The `aria-describedby` id list, or `undefined` when there is nothing to describe it. */
78
+ describedBy?: string;
79
+ /** The effective disabled state: the control's OR the field's. */
80
+ disabled: boolean;
81
+ /** The effective invalid state: the control's OR the field's. */
82
+ invalid: boolean;
83
+ /** The effective required state: the control's OR the field's. */
84
+ required: boolean;
85
+ }
86
+
87
+ /**
88
+ * Resolve what an enclosing `Field` contributes to one control.
89
+ *
90
+ * ```tsx
91
+ * const field = useFieldMembership({ accessibilityLabel, label, labelPlacement: 'adjacent', disabled });
92
+ * <Pressable
93
+ * nativeID={field.nativeID}
94
+ * accessibilityLabel={field.accessibilityLabel}
95
+ * aria-describedby={field.describedBy}
96
+ * aria-invalid={field.invalid || undefined}
97
+ * disabled={field.disabled}
98
+ * />
99
+ * ```
100
+ *
101
+ * Outside a `Field` it returns the control's own values unchanged, so a control
102
+ * can call it unconditionally.
103
+ */
104
+ export function useFieldMembership(input: FieldMembershipInput = {}): FieldMembership {
105
+ const field = useFieldControl();
106
+ const {
107
+ accessibilityLabel,
108
+ label,
109
+ labelPlacement = 'stacked',
110
+ disabled = false,
111
+ invalid = false,
112
+ required = false,
113
+ nativeID,
114
+ describedBy,
115
+ } = input;
116
+
117
+ const fieldLabel = field?.labelText;
118
+ const fieldDescribedBy = field?.describedBy;
119
+ const fieldControlId = field?.controlId;
120
+ const fieldDisabled = field?.disabled === true;
121
+ const fieldInvalid = field?.invalid === true;
122
+ const fieldRequired = field?.required === true;
123
+
124
+ return useMemo<FieldMembership>(() => {
125
+ const own = label ?? undefined;
126
+ const name =
127
+ accessibilityLabel ??
128
+ (labelPlacement === 'adjacent' ? (own ?? fieldLabel) : (fieldLabel ?? own));
129
+
130
+ // An id LIST: a control that already points at its own hint keeps it, and
131
+ // the field's error is added rather than swapped in. Duplicates are
132
+ // dropped — an id named twice is read twice by some screen readers.
133
+ const described = [describedBy, fieldDescribedBy].filter(
134
+ (id): id is string => typeof id === 'string' && id.length > 0,
135
+ );
136
+ const uniqueDescribed = Array.from(new Set(described.flatMap((ids) => ids.split(/\s+/))));
137
+
138
+ return {
139
+ inField: field != null,
140
+ nativeID: nativeID ?? fieldControlId,
141
+ accessibilityLabel: name,
142
+ describedBy: uniqueDescribed.length > 0 ? uniqueDescribed.join(' ') : undefined,
143
+ disabled: disabled || fieldDisabled,
144
+ invalid: invalid || fieldInvalid,
145
+ required: required || fieldRequired,
146
+ };
147
+ }, [
148
+ field,
149
+ accessibilityLabel,
150
+ label,
151
+ labelPlacement,
152
+ disabled,
153
+ invalid,
154
+ required,
155
+ nativeID,
156
+ describedBy,
157
+ fieldLabel,
158
+ fieldDescribedBy,
159
+ fieldControlId,
160
+ fieldDisabled,
161
+ fieldInvalid,
162
+ fieldRequired,
163
+ ]);
164
+ }
@@ -92,6 +92,7 @@ type StaggerState = 'shown' | 'hiding' | 'hidden';
92
92
 
93
93
  import { formatFileSize } from './shared';
94
94
  import { DISABLED_OPACITY } from '../styles/tokens';
95
+ import { useFieldMembership } from '../field/membership';
95
96
 
96
97
  export { formatFileSize };
97
98
 
@@ -445,12 +446,19 @@ const FileUploadComponent = function FileUpload({
445
446
  maxBytes = DEFAULT_MAX_BYTES,
446
447
  renderFileIcon,
447
448
  labels: labelOverrides,
448
- disabled = false,
449
- accessibilityLabel = 'Upload a file',
449
+ disabled: disabledProp = false,
450
+ accessibilityLabel,
450
451
  style,
451
452
  testID,
452
453
  }: FileUploadProps) {
453
454
  const theme = useTheme();
455
+ // The drop zone draws a prompt but names itself with a prop, so a `Field`
456
+ // around it is the other place the name can come from — and the field's
457
+ // `disabled` blocks the picker whatever the caller passed. `'Upload a file'`
458
+ // stays the last resort rather than a default that would outrank a label the
459
+ // user can read.
460
+ const field = useFieldMembership({ accessibilityLabel, disabled: disabledProp });
461
+ const disabled = field.disabled;
454
462
  useInteractiveWebCss(STYLE_ID, BLOOM_FILE_UPLOAD_CSS);
455
463
  const reducedMotion = useReducedMotion();
456
464
  const paint = useMemo(() => resolveFileUploadPaint(theme), [theme]);
@@ -644,7 +652,10 @@ const FileUploadComponent = function FileUpload({
644
652
  ref={rootRef}
645
653
  {...webDataSet({ bloomFileUpload: busy ? 'busy' : 'idle' })}
646
654
  accessibilityRole="button"
647
- accessibilityLabel={accessibilityLabel}
655
+ nativeID={field.nativeID}
656
+ accessibilityLabel={field.accessibilityLabel ?? 'Upload a file'}
657
+ aria-describedby={field.describedBy}
658
+ aria-invalid={field.invalid || undefined}
648
659
  aria-busy={busy}
649
660
  disabled={disabled}
650
661
  focusable={!blocked}
@@ -162,6 +162,19 @@ export interface TriggerHandleProps {
162
162
  accessibilityLabel?: string;
163
163
  accessibilityRole?: 'button';
164
164
  'aria-expanded'?: boolean;
165
+ /**
166
+ * The trigger's id, so an enclosing `Field`'s label can point at it.
167
+ *
168
+ * Only a trigger that IS a form control sets it — `Select` does; a `Popover`
169
+ * or a `DropdownMenu` inside a field is not the field's control and must not
170
+ * take its id. `TriggerSlot` therefore does not read the field context
171
+ * itself; each family decides whether it is the control.
172
+ */
173
+ nativeID?: string;
174
+ /** The field's hint or error, for a trigger that is a form control. */
175
+ 'aria-describedby'?: string;
176
+ /** The field's invalid state, for a trigger that is a form control. */
177
+ 'aria-invalid'?: boolean;
165
178
  /**
166
179
  * What this trigger OPENS. Set by each family from its own constant in
167
180
  * `floating/constants.ts`, never defaulted here — the five families open three
package/src/index.ts CHANGED
@@ -167,6 +167,7 @@ export {
167
167
  useTopEdgeInset,
168
168
  ScrollOffsetProvider,
169
169
  useScrollOffset,
170
+ ScreenScope,
170
171
  } from './layout';
171
172
  export { Fab } from './fab';
172
173
  export type { FabProps, FabVariant, FabSize, FabPlacement } from './fab';
@@ -282,8 +283,14 @@ export * from './date-picker';
282
283
  export { Search } from './search';
283
284
  export { Label } from './label';
284
285
  export type { LabelProps } from './label';
285
- export { Field } from './field';
286
- export type { FieldProps } from './field';
286
+ export { Field, useFieldControl, useFieldMembership } from './field';
287
+ export type {
288
+ FieldControlValue,
289
+ FieldLabelPlacement,
290
+ FieldMembership,
291
+ FieldMembershipInput,
292
+ FieldProps,
293
+ } from './field';
287
294
  export { InputGroup, InputGroupAddon } from './input-group';
288
295
  export type { InputGroupProps, InputGroupAddonProps } from './input-group';
289
296
  export { RangeSlider, Slider } from './slider';
package/src/index.web.ts CHANGED
@@ -172,6 +172,7 @@ export {
172
172
  useTopEdgeInset,
173
173
  ScrollOffsetProvider,
174
174
  useScrollOffset,
175
+ ScreenScope,
175
176
  } from './layout';
176
177
  export { Fab } from './fab/index.web';
177
178
  export type { FabProps, FabVariant, FabSize, FabPlacement } from './fab/index.web';
@@ -287,8 +288,14 @@ export * from './date-picker';
287
288
  export { Search } from './search';
288
289
  export { Label } from './label';
289
290
  export type { LabelProps } from './label';
290
- export { Field } from './field';
291
- export type { FieldProps } from './field';
291
+ export { Field, useFieldControl, useFieldMembership } from './field';
292
+ export type {
293
+ FieldControlValue,
294
+ FieldLabelPlacement,
295
+ FieldMembership,
296
+ FieldMembershipInput,
297
+ FieldProps,
298
+ } from './field';
292
299
  export { InputGroup, InputGroupAddon } from './input-group';
293
300
  export type { InputGroupProps, InputGroupAddonProps } from './input-group';
294
301
  export { RangeSlider, Slider } from './slider';
@@ -24,6 +24,7 @@ import {
24
24
  } from '../text-field/shared';
25
25
  import { TextFieldGroupContext } from '../text-field/TextField';
26
26
  import type { InputGroupAddonProps, InputGroupProps } from './types';
27
+ import { useFieldMembership } from '../field/membership';
27
28
 
28
29
  /**
29
30
  * The group is the input shell (`base/input/input.tsx`) with its adornment
@@ -149,12 +150,21 @@ InputGroupAddon.displayName = 'InputGroupAddon';
149
150
  */
150
151
  const InputGroupComponent = function InputGroup({
151
152
  children,
152
- isInvalid = false,
153
- disabled = false,
153
+ isInvalid: isInvalidProp = false,
154
+ disabled: disabledProp = false,
154
155
  size = 'md',
155
156
  style,
156
157
  testID,
157
158
  }: InputGroupProps) {
159
+ // A group holding a control and its addons is ONE control as far as a `Field`
160
+ // is concerned: the field's `disabled` and invalid state reach every member
161
+ // through the group's own contexts, including an addon's `Button`, which reads
162
+ // neither the field nor the text-field group. The group publishes no name — it
163
+ // does not know which of its children is the control the label points at, so
164
+ // that stays the field's `nativeID` and the input's to apply.
165
+ const member = useFieldMembership({ disabled: disabledProp, invalid: isInvalidProp });
166
+ const disabled = member.disabled;
167
+ const isInvalid = member.invalid;
158
168
  const cfg = SIZE_CONFIG[size];
159
169
  const { state: focused, onIn: onFocus, onOut: onBlur } = useInteractionState();
160
170
  const { state: hovered, onIn: onHoverIn, onOut: onHoverOut } =
@@ -211,6 +221,7 @@ const InputGroupComponent = function InputGroup({
211
221
  // react-native-web has no `disabled` prop to derive the attribute from
212
222
  // and never reads `accessibilityState`. React Native folds it back.
213
223
  aria-disabled={disabled || undefined}
224
+ aria-invalid={isInvalid || undefined}
214
225
  // Capture focus bubbling from a nested input so the whole chrome
215
226
  // reflects focus — RN-web bubbles focus/blur, native does not but a
216
227
  // nested TextInput's own focus ring is sufficient there.
@@ -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
  }