@object-ui/fields 17.5.0 → 17.6.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 (229) hide show
  1. package/CHANGELOG.md +714 -0
  2. package/dist/FieldEditWidget.d.ts +16 -2
  3. package/dist/field-type-alias.d.ts +16 -0
  4. package/dist/index.cjs +4 -4
  5. package/dist/index.css +27 -9
  6. package/dist/index.d.ts +425 -72
  7. package/dist/index.js +2530 -2228
  8. package/dist/widgets/AddressField.d.ts +3 -3
  9. package/dist/widgets/AutoNumberField.d.ts +1 -1
  10. package/dist/widgets/AvatarField.d.ts +1 -1
  11. package/dist/widgets/BooleanField.d.ts +1 -1
  12. package/dist/widgets/CapabilityMultiSelectField.d.ts +1 -1
  13. package/dist/widgets/CheckboxesField.d.ts +1 -1
  14. package/dist/widgets/CodeField.d.ts +1 -1
  15. package/dist/widgets/ColorField.d.ts +1 -1
  16. package/dist/widgets/CurrencyField.d.ts +1 -1
  17. package/dist/widgets/DateField.d.ts +1 -1
  18. package/dist/widgets/DateTimeField.d.ts +1 -1
  19. package/dist/widgets/EmailField.d.ts +1 -1
  20. package/dist/widgets/FileField.d.ts +18 -2
  21. package/dist/widgets/FilterConditionField.d.ts +28 -1
  22. package/dist/widgets/FormulaField.d.ts +1 -1
  23. package/dist/widgets/FullscreenFieldEditor.d.ts +9 -2
  24. package/dist/widgets/GeolocationField.d.ts +1 -1
  25. package/dist/widgets/GridField.d.ts +56 -4
  26. package/dist/widgets/ImageField.d.ts +1 -1
  27. package/dist/widgets/LocationField.d.ts +1 -1
  28. package/dist/widgets/LookupField.d.ts +1 -1
  29. package/dist/widgets/MasterDetailField.d.ts +1 -1
  30. package/dist/widgets/MultiSelectField.d.ts +1 -1
  31. package/dist/widgets/NumberField.d.ts +1 -1
  32. package/dist/widgets/ObjectField.d.ts +1 -1
  33. package/dist/widgets/ObjectRefField.d.ts +1 -1
  34. package/dist/widgets/OptionsEmptyState.d.ts +13 -7
  35. package/dist/widgets/PasswordField.d.ts +1 -1
  36. package/dist/widgets/PercentField.d.ts +1 -1
  37. package/dist/widgets/PhoneField.d.ts +1 -1
  38. package/dist/widgets/QRCodeField.d.ts +1 -1
  39. package/dist/widgets/RadioField.d.ts +1 -1
  40. package/dist/widgets/RatingField.d.ts +1 -1
  41. package/dist/widgets/RecipientPickerField.d.ts +1 -1
  42. package/dist/widgets/RecordPickerDialog.d.ts +2 -4
  43. package/dist/widgets/RichTextField.d.ts +37 -1
  44. package/dist/widgets/SelectField.d.ts +1 -1
  45. package/dist/widgets/SignatureField.d.ts +32 -2
  46. package/dist/widgets/SliderField.d.ts +37 -2
  47. package/dist/widgets/SummaryField.d.ts +1 -1
  48. package/dist/widgets/TagsField.d.ts +1 -1
  49. package/dist/widgets/TextAreaField.d.ts +6 -2
  50. package/dist/widgets/TextField.d.ts +1 -1
  51. package/dist/widgets/TimeField.d.ts +1 -1
  52. package/dist/widgets/UrlField.d.ts +1 -1
  53. package/dist/widgets/UserField.d.ts +1 -1
  54. package/dist/widgets/VectorField.d.ts +1 -1
  55. package/dist/widgets/_cell-renderer-bridge.d.ts +1 -1
  56. package/dist/widgets/address-format.d.ts +27 -7
  57. package/dist/widgets/lookupColumnDisplay.d.ts +93 -0
  58. package/dist/widgets/richTextDisplay.d.ts +80 -0
  59. package/dist/widgets/toHostGroupProps.d.ts +119 -21
  60. package/dist/widgets/types.d.ts +8 -3
  61. package/package.json +9 -10
  62. package/dist/widgets/CharacterCount.d.ts +0 -56
  63. package/src/FieldEditWidget.test.ts +0 -133
  64. package/src/FieldEditWidget.tsx +0 -207
  65. package/src/__tests__/AddressCellRenderer.test.tsx +0 -166
  66. package/src/__tests__/CurrencyField.minorUnits.test.tsx +0 -191
  67. package/src/__tests__/CurrencyField.symbol.test.tsx +0 -198
  68. package/src/__tests__/DateCellRenderer.test.tsx +0 -82
  69. package/src/__tests__/NumberCellRenderer.grouping.test.tsx +0 -146
  70. package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
  71. package/src/__tests__/PercentCellRenderer.locale.test.tsx +0 -152
  72. package/src/__tests__/PercentCellRenderer.test.tsx +0 -115
  73. package/src/__tests__/boolean-label-association-e2e.test.tsx +0 -298
  74. package/src/__tests__/capability-multiselect-retired.test.ts +0 -109
  75. package/src/__tests__/cell-truncation.test.tsx +0 -96
  76. package/src/__tests__/composite-group-label-e2e.test.tsx +0 -436
  77. package/src/__tests__/composite-group-label-readonly-e2e.test.tsx +0 -403
  78. package/src/__tests__/date-formatter-residue-4272.test.ts +0 -120
  79. package/src/__tests__/date-locale-channel.test.tsx +0 -305
  80. package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
  81. package/src/__tests__/formatCurrency.centsDigit.test.tsx +0 -158
  82. package/src/__tests__/formatCurrency.minorUnits.test.tsx +0 -221
  83. package/src/__tests__/group-labelling-declaration.test.ts +0 -148
  84. package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
  85. package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
  86. package/src/__tests__/lookupCellRecordLink.test.tsx +0 -171
  87. package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
  88. package/src/__tests__/percent-cell-vs-measure-4576.test.ts +0 -115
  89. package/src/__tests__/percent-formatter-locale-4553.test.ts +0 -147
  90. package/src/__tests__/percent-tie-halfup-4590.test.ts +0 -224
  91. package/src/__tests__/registry-collision.test.ts +0 -91
  92. package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
  93. package/src/__tests__/select-label-association-e2e.test.tsx +0 -199
  94. package/src/__tests__/spec-symbol-batch7.test.ts +0 -126
  95. package/src/__tests__/validation-feedback.test.tsx +0 -361
  96. package/src/__tests__/widget-aria-invalid-e2e.test.tsx +0 -265
  97. package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +0 -338
  98. package/src/__tests__/widget-dom-leak-e2e.test.tsx +0 -357
  99. package/src/__tests__/widget-props-contract.test.tsx +0 -142
  100. package/src/coerce-safe-value.test.ts +0 -34
  101. package/src/complex-widgets.test.tsx +0 -924
  102. package/src/currency.ts +0 -144
  103. package/src/datetime-widgets.test.tsx +0 -185
  104. package/src/field-type-alias.multiple.test.ts +0 -81
  105. package/src/field-type-alias.ts +0 -156
  106. package/src/field-type-coverage.test.ts +0 -67
  107. package/src/index.css +0 -44
  108. package/src/index.tsx +0 -2745
  109. package/src/standard-widgets.test.tsx +0 -364
  110. package/src/widgets/AddressField.postalCode.test.tsx +0 -257
  111. package/src/widgets/AddressField.tsx +0 -182
  112. package/src/widgets/AddressField.uniqueIds.test.tsx +0 -93
  113. package/src/widgets/AutoNumberField.tsx +0 -18
  114. package/src/widgets/AvatarField.tsx +0 -117
  115. package/src/widgets/BooleanField.tsx +0 -86
  116. package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
  117. package/src/widgets/CapabilityMultiSelectField.tsx +0 -241
  118. package/src/widgets/CharacterCount.tsx +0 -263
  119. package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
  120. package/src/widgets/CheckboxesField.tsx +0 -169
  121. package/src/widgets/CodeField.tsx +0 -41
  122. package/src/widgets/ColorField.tsx +0 -53
  123. package/src/widgets/CurrencyField.tsx +0 -135
  124. package/src/widgets/DateField.tsx +0 -40
  125. package/src/widgets/DateTimeField.tsx +0 -46
  126. package/src/widgets/EmailField.tsx +0 -37
  127. package/src/widgets/FileField.tsx +0 -512
  128. package/src/widgets/FilterConditionField.tsx +0 -454
  129. package/src/widgets/FormulaField.tsx +0 -38
  130. package/src/widgets/FullscreenFieldEditor.tsx +0 -60
  131. package/src/widgets/GeolocationField.tsx +0 -193
  132. package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
  133. package/src/widgets/GridField.test.tsx +0 -632
  134. package/src/widgets/GridField.tsx +0 -1070
  135. package/src/widgets/ImageCropperDialog.tsx +0 -245
  136. package/src/widgets/ImageField.maxSize.test.tsx +0 -154
  137. package/src/widgets/ImageField.tsx +0 -305
  138. package/src/widgets/ImageLightbox.test.tsx +0 -52
  139. package/src/widgets/ImageLightbox.tsx +0 -108
  140. package/src/widgets/LocationField.tsx +0 -57
  141. package/src/widgets/LookupField.dependsOn.test.tsx +0 -233
  142. package/src/widgets/LookupField.gateHintLabel.test.tsx +0 -135
  143. package/src/widgets/LookupField.hydration.test.tsx +0 -136
  144. package/src/widgets/LookupField.idField.test.tsx +0 -61
  145. package/src/widgets/LookupField.tsx +0 -1295
  146. package/src/widgets/MarkdownContent.tsx +0 -19
  147. package/src/widgets/MasterDetailField.tsx +0 -131
  148. package/src/widgets/MultiSelectField.cascade.test.tsx +0 -143
  149. package/src/widgets/MultiSelectField.tsx +0 -196
  150. package/src/widgets/NumberField.tsx +0 -49
  151. package/src/widgets/ObjectField.tsx +0 -94
  152. package/src/widgets/ObjectRefField.tsx +0 -121
  153. package/src/widgets/OptionsEmptyState.no-provider.test.tsx +0 -88
  154. package/src/widgets/OptionsEmptyState.test.tsx +0 -169
  155. package/src/widgets/OptionsEmptyState.tsx +0 -99
  156. package/src/widgets/PasswordField.tsx +0 -49
  157. package/src/widgets/PeoplePicker.test.tsx +0 -360
  158. package/src/widgets/PeoplePicker.tsx +0 -579
  159. package/src/widgets/PeoplePickerComponents.test.tsx +0 -104
  160. package/src/widgets/PercentField.tsx +0 -95
  161. package/src/widgets/PersonRow.tsx +0 -117
  162. package/src/widgets/PhoneField.tsx +0 -37
  163. package/src/widgets/QRCodeField.tsx +0 -103
  164. package/src/widgets/RadioField.cascade.test.tsx +0 -141
  165. package/src/widgets/RadioField.tsx +0 -143
  166. package/src/widgets/RatingField.tsx +0 -94
  167. package/src/widgets/RecipientPickerField.test.tsx +0 -157
  168. package/src/widgets/RecipientPickerField.tsx +0 -188
  169. package/src/widgets/RecordPickerDialog.baseFilterShapes.test.tsx +0 -191
  170. package/src/widgets/RecordPickerDialog.dateLocale.test.tsx +0 -124
  171. package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
  172. package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
  173. package/src/widgets/RecordPickerDialog.tsx +0 -1365
  174. package/src/widgets/RichTextField.tsx +0 -176
  175. package/src/widgets/SelectField.cascade.test.tsx +0 -138
  176. package/src/widgets/SelectField.tsx +0 -193
  177. package/src/widgets/SelectionTray.tsx +0 -110
  178. package/src/widgets/SignatureField.tsx +0 -140
  179. package/src/widgets/SliderField.tsx +0 -39
  180. package/src/widgets/SummaryField.tsx +0 -31
  181. package/src/widgets/TagsField.placeholder.no-provider.test.tsx +0 -68
  182. package/src/widgets/TagsField.placeholder.test.tsx +0 -95
  183. package/src/widgets/TagsField.tsx +0 -88
  184. package/src/widgets/TextAreaField.tsx +0 -213
  185. package/src/widgets/TextField.tsx +0 -45
  186. package/src/widgets/TimeField.tsx +0 -31
  187. package/src/widgets/UrlField.tsx +0 -46
  188. package/src/widgets/UserField.tsx +0 -59
  189. package/src/widgets/UserFieldPicker.test.tsx +0 -78
  190. package/src/widgets/VectorField.tsx +0 -29
  191. package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +0 -66
  192. package/src/widgets/__tests__/FilterConditionField.operators.test.ts +0 -166
  193. package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +0 -248
  194. package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +0 -116
  195. package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +0 -244
  196. package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +0 -195
  197. package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +0 -417
  198. package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +0 -287
  199. package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +0 -160
  200. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +0 -161
  201. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +0 -346
  202. package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +0 -246
  203. package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +0 -175
  204. package/src/widgets/_cell-renderer-bridge.ts +0 -19
  205. package/src/widgets/address-format.ts +0 -120
  206. package/src/widgets/deriveLookupColumns.test.ts +0 -112
  207. package/src/widgets/deriveLookupColumns.ts +0 -108
  208. package/src/widgets/file-size-guard.ts +0 -55
  209. package/src/widgets/file-value.test.ts +0 -230
  210. package/src/widgets/file-value.ts +0 -246
  211. package/src/widgets/nativeDateValue.test.ts +0 -115
  212. package/src/widgets/nativeDateValue.ts +0 -112
  213. package/src/widgets/openNativePicker.ts +0 -18
  214. package/src/widgets/optionWidgets.gatedOptions.test.tsx +0 -267
  215. package/src/widgets/optionWidgets.unconfiguredOptions.test.tsx +0 -150
  216. package/src/widgets/personDisplay.test.ts +0 -113
  217. package/src/widgets/personDisplay.ts +0 -92
  218. package/src/widgets/recentLookups.test.ts +0 -45
  219. package/src/widgets/recentLookups.ts +0 -47
  220. package/src/widgets/toDomProps.ts +0 -204
  221. package/src/widgets/toHostGroupProps.ts +0 -102
  222. package/src/widgets/types.ts +0 -242
  223. package/src/widgets/useCascadingOptions.ts +0 -46
  224. package/src/widgets/useFieldTranslation.ts +0 -132
  225. package/src/widgets/useRecordQuery.test.ts +0 -174
  226. package/src/widgets/useRecordQuery.ts +0 -291
  227. package/src/widgets/useUploadingSignal.test.tsx +0 -62
  228. package/src/widgets/useUploadingSignal.ts +0 -20
  229. package/src/withFieldCarrier.tsx +0 -75
@@ -1,193 +0,0 @@
1
- import React, { useId } from 'react';
2
- import { Input, Button, Label, EmptyValue } from '@object-ui/components';
3
- import { MapPin, Crosshair } from 'lucide-react';
4
- import { FieldWidgetComponentProps } from './types';
5
- import { toDomProps } from './toDomProps';
6
- import { toHostGroupProps } from './toHostGroupProps';
7
-
8
- /**
9
- * Geolocation data structure
10
- */
11
- export interface GeolocationValue {
12
- latitude?: number;
13
- longitude?: number;
14
- accuracy?: number;
15
- }
16
-
17
- /**
18
- * Geolocation field widget - provides a location picker with coordinates
19
- * Supports manual entry and browser geolocation API
20
- */
21
- export function GeolocationField({ value, onChange, field, readonly, error, ...props }: FieldWidgetComponentProps<GeolocationValue>) {
22
- const [isLoading, setIsLoading] = React.useState(false);
23
- const location = value || {};
24
- // DOM pass-through (objectui#3318): the whitelist spread goes onto the FIRST
25
- // sub-input (latitude); the composite's validation state goes onto BOTH
26
- // focusable sub-inputs via `aria-invalid={!!error}`.
27
- //
28
- // `id` and `aria-labelledby` are held back and land on the group CONTAINER
29
- // instead (objectui#3961) — they address the whole field, not the latitude box.
30
- // The host `id` never reached the DOM before: it was overwritten one line later
31
- // by `id={subId('latitude')}` (objectui#3343), leaving the form's group label
32
- // pointing at nothing. And `aria-labelledby` on the latitude input would
33
- // OVERRIDE its own "Latitude" label with the field name. `aria-describedby` and
34
- // the rest stay on the first sub-input, where focus can reach them.
35
- const {
36
- id: _hostId,
37
- 'aria-labelledby': _hostLabelledBy,
38
- ...domProps
39
- } = toDomProps(props);
40
- // The pair held back above, in the one spelling every group-labelled widget
41
- // uses for it — and computed HERE, before the readonly early return below,
42
- // because that return used to drop both keys on the floor: a published label
43
- // id with no consumer in the document (objectui#3990). See `toHostGroupProps`.
44
- const hostGroupProps = toHostGroupProps(props);
45
- // Sub-input ids (objectui#3343): `useId()` prefix + sub-field name — the
46
- // `groupId` paradigm of RadioField / CheckboxesField. Hardcoded literals
47
- // ("latitude" / "longitude") collide as soon as a form renders two
48
- // geolocation fields, and every label's htmlFor then resolves to the
49
- // FIRST match.
50
- const groupId = useId();
51
- const subId = (name: keyof GeolocationValue) => `${groupId}-${name}`;
52
-
53
- const handleFieldChange = (fieldName: keyof GeolocationValue, fieldValue: string) => {
54
- onChange({
55
- ...location,
56
- [fieldName]: fieldValue ? Number(fieldValue) : undefined,
57
- });
58
- };
59
-
60
- const getCurrentLocation = () => {
61
- if (!navigator.geolocation) {
62
- console.error('Geolocation is not supported by this browser');
63
- return;
64
- }
65
-
66
- setIsLoading(true);
67
- navigator.geolocation.getCurrentPosition(
68
- (position) => {
69
- onChange({
70
- latitude: position.coords.latitude,
71
- longitude: position.coords.longitude,
72
- accuracy: position.coords.accuracy,
73
- });
74
- setIsLoading(false);
75
- },
76
- (error) => {
77
- console.error('Error getting location:', error.message);
78
- setIsLoading(false);
79
- },
80
- {
81
- enableHighAccuracy: true,
82
- timeout: 5000,
83
- maximumAge: 0,
84
- }
85
- );
86
- };
87
-
88
- const formatLocation = (loc: GeolocationValue): string => {
89
- if (!loc.latitude || !loc.longitude) return '';
90
- return `${loc.latitude.toFixed(6)}, ${loc.longitude.toFixed(6)}`;
91
- };
92
-
93
- const openInMaps = () => {
94
- if (!location.latitude || !location.longitude) return;
95
- const url = `https://www.google.com/maps?q=${location.latitude},${location.longitude}`;
96
- window.open(url, '_blank');
97
- };
98
-
99
- if (readonly) {
100
- // Readonly the composite collapses to the formatted coordinates (plus the
101
- // "View on map" link), so THAT row is the surface the host label names
102
- // (objectui#3990). The `EmptyValue` placeholder sits inside it, so it needs
103
- // nothing of its own.
104
- const formatted = formatLocation(location);
105
- return (
106
- <div {...hostGroupProps} className="flex items-center gap-2">
107
- <MapPin className="w-4 h-4 text-muted-foreground" />
108
- {formatted ? <span className="text-sm">{formatted}</span> : <EmptyValue />}
109
- {location.latitude && location.longitude && (
110
- <Button
111
- type="button"
112
- variant="link"
113
- size="sm"
114
- onClick={openInMaps}
115
- className="p-0 h-auto"
116
- >
117
- View on map
118
- </Button>
119
- )}
120
- </div>
121
- );
122
- }
123
-
124
- return (
125
- // `role="group"` only when a host actually named this container
126
- // (objectui#3961); standalone rendering stays exactly as it was. That
127
- // condition now lives in `toHostGroupProps` so this container and the
128
- // readonly row above cannot answer differently (objectui#3990).
129
- <div className="space-y-3" {...hostGroupProps}>
130
- <div className="flex items-center gap-2">
131
- <Button
132
- type="button"
133
- variant="outline"
134
- size="sm"
135
- onClick={getCurrentLocation}
136
- disabled={readonly || isLoading}
137
- >
138
- <Crosshair className="w-4 h-4 mr-2" />
139
- {isLoading ? 'Getting location...' : 'Use Current Location'}
140
- </Button>
141
- {location.latitude && location.longitude && (
142
- <Button
143
- type="button"
144
- variant="link"
145
- size="sm"
146
- onClick={openInMaps}
147
- >
148
- <MapPin className="w-4 h-4 mr-2" />
149
- View on map
150
- </Button>
151
- )}
152
- </div>
153
-
154
- <div className="grid grid-cols-2 gap-3">
155
- <div>
156
- <Label htmlFor={subId('latitude')} className="text-xs">Latitude</Label>
157
- <Input
158
- {...domProps}
159
- id={subId('latitude')}
160
- type="number"
161
- value={location.latitude ?? ''}
162
- onChange={(e) => handleFieldChange('latitude', e.target.value)}
163
- placeholder="37.7749"
164
- disabled={readonly || props.disabled}
165
- step="any"
166
- className={props.className}
167
- aria-invalid={!!error}
168
- />
169
- </div>
170
-
171
- <div>
172
- <Label htmlFor={subId('longitude')} className="text-xs">Longitude</Label>
173
- <Input
174
- id={subId('longitude')}
175
- type="number"
176
- value={location.longitude ?? ''}
177
- onChange={(e) => handleFieldChange('longitude', e.target.value)}
178
- placeholder="-122.4194"
179
- disabled={readonly || props.disabled}
180
- step="any"
181
- aria-invalid={!!error}
182
- />
183
- </div>
184
- </div>
185
-
186
- {location.accuracy && (
187
- <p className="text-xs text-muted-foreground">
188
- Accuracy: ±{location.accuracy.toFixed(0)}m
189
- </p>
190
- )}
191
- </div>
192
- );
193
- }
@@ -1,88 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- /**
10
- * Unique sub-input ids for the composite `geolocation` widget
11
- * (objectui#3343).
12
- *
13
- * The widget used to hardcode literal ids ("latitude" / "longitude") on its
14
- * sub-inputs. Two geolocation fields in one form — e.g. origin + destination
15
- * — then produced duplicate DOM ids, and every `Label htmlFor` resolved to
16
- * the FIRST match in the document: each sub-label of the second field
17
- * clicked/announced the first field's input.
18
- *
19
- * The fix follows the `groupId` paradigm of RadioField / CheckboxesField:
20
- * a `useId()` prefix + the sub-field name. These tests pin it by rendering
21
- * TWO instances in one form:
22
- *
23
- * 1. every `[id]` in the document is globally unique (red again if anyone
24
- * reverts to hardcoded literals — both instances would emit
25
- * id="latitude");
26
- * 2. each sub-label of the SECOND instance resolves — via the exact
27
- * mechanism browsers use for label activation, `for` → getElementById —
28
- * to an input inside its OWN instance, and focusing it lands there.
29
- */
30
- import { describe, it, expect, vi } from 'vitest';
31
- import React from 'react';
32
- import { render, within } from '@testing-library/react';
33
- import '@testing-library/jest-dom';
34
- import { GeolocationField } from './GeolocationField';
35
-
36
- const field = { name: 'location', type: 'geolocation' } as any;
37
-
38
- const SUB_LABELS = ['Latitude', 'Longitude'];
39
-
40
- function renderTwoInOneForm() {
41
- return render(
42
- <form>
43
- <div data-testid="geo-a">
44
- <GeolocationField value={{}} onChange={vi.fn()} field={field} />
45
- </div>
46
- <div data-testid="geo-b">
47
- <GeolocationField value={{}} onChange={vi.fn()} field={field} />
48
- </div>
49
- </form>,
50
- );
51
- }
52
-
53
- describe('GeolocationField — unique sub-input ids (objectui#3343)', () => {
54
- it('two geolocation fields in one form produce globally unique DOM ids', () => {
55
- const { baseElement } = renderTwoInOneForm();
56
- const ids = Array.from(baseElement.querySelectorAll('[id]')).map((el) => el.id);
57
- // 2 sub-inputs per instance — both instances must actually render ids.
58
- expect(ids.length).toBeGreaterThanOrEqual(4);
59
- const duplicates = ids.filter((id, i) => ids.indexOf(id) !== i);
60
- expect(duplicates).toEqual([]);
61
- });
62
-
63
- it("each sub-label of the SECOND field focuses that field's own input", () => {
64
- const { getByTestId } = renderTwoInOneForm();
65
- const first = getByTestId('geo-a');
66
- const second = getByTestId('geo-b');
67
-
68
- for (const text of SUB_LABELS) {
69
- const label = within(second).getByText(text);
70
- const forId = label.getAttribute('for');
71
- expect(forId, `label "${text}" must carry htmlFor`).toBeTruthy();
72
-
73
- // Exactly what the browser does on label click / SR announcement:
74
- // resolve `for` against the document.
75
- const control = document.getElementById(forId!);
76
- expect(control, `htmlFor of "${text}" must resolve`).not.toBeNull();
77
- expect(control!.tagName).toBe('INPUT');
78
- expect(
79
- second.contains(control),
80
- `"${text}" must resolve into its OWN field, not the first one`,
81
- ).toBe(true);
82
- expect(first.contains(control)).toBe(false);
83
-
84
- (control as HTMLInputElement).focus();
85
- expect(control).toHaveFocus();
86
- }
87
- });
88
- });