@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,95 +0,0 @@
1
- import React from 'react';
2
- import { Input, Slider, EmptyValue } from '@object-ui/components';
3
- import { FieldWidgetComponentProps } from './types';
4
- import { toDomProps } from './toDomProps';
5
-
6
- /**
7
- * PercentField - Percentage input with configurable decimal precision
8
- * Stores values as decimals (0-1) and displays as percentages (0-100%)
9
- * Includes a slider for interactive control.
10
- */
11
- export function PercentField({ value, onChange, field, readonly, error, className, ...props }: FieldWidgetComponentProps<number>) {
12
- const percentField = field as any;
13
- const precision = percentField?.precision ?? 2;
14
-
15
- // Convention detection. A field declaring `max > 1` (e.g. `max: 100`) stores
16
- // WHOLE-NUMBER percents (0–100); otherwise values are FRACTIONS (0–1) shown
17
- // as 0–100%. This matches the read-side formatter so the edit widget agrees
18
- // with display — and, crucially, keeps the rendered <input> within its `max`
19
- // (a whole-number 50 must show "50", not "5000", or HTML5 constraint
20
- // validation marks the field `:invalid` and blocks the whole form's submit).
21
- const maxAttr = typeof percentField?.max === 'number' ? (percentField.max as number) : undefined;
22
- const whole = maxAttr != null && maxAttr > 1;
23
- const toDisplay = (v: number) => (whole ? v : v * 100);
24
- const fromDisplay = (n: number) => (whole ? n : n / 100);
25
- const sliderMax = whole ? maxAttr! : 100;
26
-
27
- if (readonly) {
28
- if (value == null) return <EmptyValue />;
29
- return (
30
- <span className="text-sm font-medium tabular-nums">
31
- {toDisplay(value).toFixed(precision)}%
32
- </span>
33
- );
34
- }
35
-
36
- // Convert between stored value and 0–100 display value
37
- const displayValue = value != null ? toDisplay(value) : '';
38
- const sliderValue = value != null ? toDisplay(value) : 0;
39
-
40
- const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
41
- if (e.target.value === '') {
42
- onChange(null as any);
43
- return;
44
- }
45
- const parsed = parseFloat(e.target.value);
46
- const val = isNaN(parsed) ? null : fromDisplay(parsed);
47
- onChange(val as any);
48
- };
49
-
50
- const handleSliderChange = (values: number[]) => {
51
- if (readonly || props.disabled) return;
52
- if (!Array.isArray(values) || values.length === 0) {
53
- onChange(null as any);
54
- return;
55
- }
56
- const raw = values[0];
57
- const nextValue = typeof raw === 'number' ? fromDisplay(raw) : null;
58
- onChange(nextValue as any);
59
- };
60
-
61
- // Derive slider step from precision so slider granularity matches the input
62
- const sliderStep = Math.pow(10, -precision);
63
-
64
- return (
65
- <div className="space-y-2">
66
- <div className="relative">
67
- <Input
68
- {...toDomProps(props)}
69
- type="number"
70
- value={displayValue}
71
- onChange={handleChange}
72
- placeholder={percentField?.placeholder || '0'}
73
- disabled={readonly || props.disabled}
74
- className={`pr-8 ${className || ''}`}
75
- step={Math.pow(10, -precision).toFixed(precision)}
76
- aria-invalid={!!error}
77
- />
78
- <span className="absolute right-3 top-1/2 -translate-y-1/2 text-sm text-gray-500">
79
- %
80
- </span>
81
- </div>
82
- <Slider
83
- value={[sliderValue]}
84
- onValueChange={handleSliderChange}
85
- min={0}
86
- max={sliderMax}
87
- step={sliderStep}
88
- disabled={readonly || props.disabled}
89
- className="w-full"
90
- aria-label="Percentage"
91
- data-testid="percent-slider"
92
- />
93
- </div>
94
- );
95
- }
@@ -1,117 +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
- import React, { useEffect, useRef } from 'react';
10
- import { Avatar, AvatarFallback, AvatarImage, cn } from '@object-ui/components';
11
- import { Check } from 'lucide-react';
12
- import {
13
- getPersonName,
14
- getPersonInitials,
15
- getPersonSubtitle,
16
- getPersonAvatarUrl,
17
- matchRanges,
18
- } from './personDisplay';
19
-
20
- /**
21
- * A rich, single-line candidate row for the search-first PeoplePicker:
22
- * avatar (image with initials fallback) + name + subtitle (department · email).
23
- * The subtitle is what lets search stand in for an org tree — it disambiguates
24
- * same-named people inline. The typed query is highlighted in both lines.
25
- * Selectable/toggleable; a Check marks the selected state, and `active` reflects
26
- * the keyboard cursor (scrolls itself into view).
27
- */
28
- export interface PersonRowProps {
29
- record: any;
30
- displayField?: string;
31
- subtitleFields?: string[];
32
- avatarField?: string;
33
- selected?: boolean;
34
- /** Keyboard cursor is on this row — highlight + scroll into view. */
35
- active?: boolean;
36
- /** Typed search term; matches are highlighted in name + subtitle. */
37
- highlightQuery?: string;
38
- onSelect?: (record: any) => void;
39
- className?: string;
40
- }
41
-
42
- /** Wrap literal matches of `query` in a subtle highlight. */
43
- function Highlighted({ text, query }: { text: string; query?: string }): React.ReactElement {
44
- const ranges = query ? matchRanges(text, query) : [];
45
- if (ranges.length === 0) return <>{text}</>;
46
- const out: React.ReactNode[] = [];
47
- let cursor = 0;
48
- ranges.forEach(([start, end], i) => {
49
- if (start > cursor) out.push(text.slice(cursor, start));
50
- out.push(
51
- <mark key={i} className="rounded-[2px] bg-primary/20 px-px text-inherit">
52
- {text.slice(start, end)}
53
- </mark>,
54
- );
55
- cursor = end;
56
- });
57
- if (cursor < text.length) out.push(text.slice(cursor));
58
- return <>{out}</>;
59
- }
60
-
61
- export function PersonRow({
62
- record,
63
- displayField = 'name',
64
- subtitleFields,
65
- avatarField = 'image',
66
- selected = false,
67
- active = false,
68
- highlightQuery,
69
- onSelect,
70
- className,
71
- }: PersonRowProps) {
72
- const name = getPersonName(record, displayField);
73
- const subtitle = getPersonSubtitle(record, subtitleFields);
74
- const avatarUrl = getPersonAvatarUrl(record, avatarField);
75
- const initials = getPersonInitials(name);
76
-
77
- const ref = useRef<HTMLButtonElement>(null);
78
- useEffect(() => {
79
- // Optional-call the method too — jsdom doesn't implement scrollIntoView.
80
- if (active) ref.current?.scrollIntoView?.({ block: 'nearest' });
81
- }, [active]);
82
-
83
- return (
84
- <button
85
- ref={ref}
86
- type="button"
87
- tabIndex={-1}
88
- onClick={() => onSelect?.(record)}
89
- aria-pressed={selected}
90
- data-testid="person-row"
91
- data-active={active || undefined}
92
- className={cn(
93
- 'flex w-full items-center gap-3 rounded-md px-2 py-1.5 text-left transition-colors',
94
- 'hover:bg-accent focus-visible:outline-none',
95
- selected && 'bg-accent',
96
- active && 'bg-accent ring-1 ring-inset ring-ring',
97
- className,
98
- )}
99
- >
100
- <Avatar className="size-9 shrink-0">
101
- {avatarUrl && <AvatarImage src={avatarUrl} alt={name} />}
102
- <AvatarFallback className="text-xs">{initials}</AvatarFallback>
103
- </Avatar>
104
- <div className="min-w-0 flex-1">
105
- <div className="truncate text-sm font-medium">
106
- {name ? <Highlighted text={name} query={highlightQuery} /> : '—'}
107
- </div>
108
- {subtitle && (
109
- <div className="truncate text-xs text-muted-foreground">
110
- <Highlighted text={subtitle} query={highlightQuery} />
111
- </div>
112
- )}
113
- </div>
114
- {selected && <Check className="size-4 shrink-0 text-primary" aria-hidden />}
115
- </button>
116
- );
117
- }
@@ -1,37 +0,0 @@
1
- import React from 'react';
2
- import { Input, EmptyValue } from '@object-ui/components';
3
- import { FieldWidgetComponentProps } from './types';
4
- import { toDomProps } from './toDomProps';
5
-
6
- /**
7
- * PhoneField - Telephone number input with tel link in readonly mode
8
- * Renders as a clickable tel link when readonly, standard phone input otherwise
9
- */
10
- export function PhoneField({ value, onChange, field, readonly, error, ...props }: FieldWidgetComponentProps<string>) {
11
- const config = field;
12
- if (readonly) {
13
- if (!value) return <EmptyValue />;
14
- return (
15
- <a
16
- href={`tel:${value}`}
17
- className="text-sm text-blue-600 hover:text-blue-800 hover:underline"
18
- >
19
- {value}
20
- </a>
21
- );
22
- }
23
-
24
- const domProps = toDomProps(props);
25
-
26
- return (
27
- <Input
28
- {...domProps}
29
- type="tel"
30
- value={value || ''}
31
- onChange={(e) => onChange(e.target.value)}
32
- placeholder={config?.placeholder || '(555) 123-4567'}
33
- disabled={readonly || domProps.disabled}
34
- aria-invalid={!!error}
35
- />
36
- );
37
- }
@@ -1,103 +0,0 @@
1
- import React from 'react';
2
- import { Input, Button, EmptyValue } from '@object-ui/components';
3
- import { QrCode, Copy } from 'lucide-react';
4
- import { FieldWidgetComponentProps } from './types';
5
- import { toDomProps } from './toDomProps';
6
-
7
- /**
8
- * QR Code field widget - generates QR codes from text
9
- * Uses a simple SVG-based QR code generator
10
- */
11
- export function QRCodeField({ value, onChange, field, readonly, error, ...props }: FieldWidgetComponentProps<string>) {
12
- const [showQR, setShowQR] = React.useState(false);
13
- const config = field;
14
-
15
- // Simple QR code generation using an external library would be ideal
16
- // For now, we'll use a service API approach or placeholder
17
- const getQRCodeUrl = (text: string): string => {
18
- // Using a simple QR code API service (you might want to replace this with a library)
19
- return `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(text)}`;
20
- };
21
-
22
- const copyToClipboard = () => {
23
- if (value) {
24
- navigator.clipboard.writeText(value);
25
- }
26
- };
27
-
28
- if (readonly) {
29
- return (
30
- <div className="flex items-center gap-3">
31
- <span className="text-sm flex-1 truncate">{value || <EmptyValue />}</span>
32
- {value && (
33
- <Button
34
- type="button"
35
- variant="outline"
36
- size="sm"
37
- onClick={() => setShowQR(!showQR)}
38
- >
39
- <QrCode className="w-4 h-4 mr-2" />
40
- {showQR ? 'Hide' : 'Show'} QR
41
- </Button>
42
- )}
43
- </div>
44
- );
45
- }
46
-
47
- return (
48
- <div className="space-y-3">
49
- <div className="flex items-center gap-2">
50
- <Input
51
- // DOM pass-through onto the real focusable control (objectui#3318).
52
- {...toDomProps(props)}
53
- type="text"
54
- value={value || ''}
55
- onChange={(e) => onChange(e.target.value)}
56
- placeholder={config?.placeholder || 'Enter text for QR code'}
57
- disabled={readonly || props.disabled}
58
- className={props.className}
59
- // AFTER the spread so this widget's own computation wins: `error`
60
- // is the published validation slot (#3222), `!!undefined` →
61
- // explicit "false".
62
- aria-invalid={!!error}
63
- />
64
- {value && (
65
- <>
66
- <Button
67
- type="button"
68
- variant="outline"
69
- size="sm"
70
- onClick={copyToClipboard}
71
- >
72
- <Copy className="w-4 h-4" />
73
- </Button>
74
- <Button
75
- type="button"
76
- variant="outline"
77
- size="sm"
78
- onClick={() => setShowQR(!showQR)}
79
- >
80
- <QrCode className="w-4 h-4" />
81
- </Button>
82
- </>
83
- )}
84
- </div>
85
-
86
- {showQR && value && (
87
- <div className="border rounded p-4 bg-white flex justify-center">
88
- <img
89
- src={getQRCodeUrl(value)}
90
- alt="QR Code"
91
- className="w-48 h-48"
92
- />
93
- </div>
94
- )}
95
-
96
- {value && (
97
- <p className="text-xs text-muted-foreground">
98
- QR code contains: {value.length} characters
99
- </p>
100
- )}
101
- </div>
102
- );
103
- }
@@ -1,141 +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
- * Cascading / role-gated `radio` options (#2715) — parity with the single
11
- * `SelectField` (ADR-0058 / #2284).
12
- *
13
- * Exercises the observable widget behaviour: the dependency gate (a "select the
14
- * parent first" empty-state) and the scalar cascade clear (a value dropped when
15
- * the offered set no longer includes it). The per-option filtering itself is
16
- * unit-tested in `@object-ui/core` (`optionRules.test.ts`); here we prove the
17
- * radio group wires `dependentValues` + predicate scope into that resolver via
18
- * the shared `useCascadingOptions` hook.
19
- */
20
- import { describe, it, expect, vi, beforeEach } from 'vitest';
21
- import React from 'react';
22
- import { render, screen } from '@testing-library/react';
23
- import '@testing-library/jest-dom';
24
- import { PredicateScopeProvider } from '@object-ui/react';
25
- import { RadioField } from './RadioField';
26
-
27
- const provinceField = {
28
- name: 'province',
29
- type: 'radio',
30
- dependsOn: 'country',
31
- options: [
32
- { label: 'Zhejiang', value: 'zj', visibleWhen: "record.country == 'cn'" },
33
- { label: 'California', value: 'ca', visibleWhen: "record.country == 'us'" },
34
- ],
35
- } as any;
36
-
37
- beforeEach(() => {
38
- vi.clearAllMocks();
39
- });
40
-
41
- describe('RadioField — dependency gating (#2715)', () => {
42
- it('gates with a "select parent first" hint while the controlling field is empty', () => {
43
- render(
44
- <RadioField
45
- value={undefined}
46
- onChange={vi.fn()}
47
- field={provinceField}
48
- {...({ name: 'province', dependentValues: {} } as any)}
49
- />,
50
- );
51
- const gate = screen.getByTestId('radio-empty-province');
52
- expect(gate).toHaveTextContent(/select country first/i);
53
- // Gated → no radios offered.
54
- expect(screen.queryAllByRole('radio')).toHaveLength(0);
55
- });
56
-
57
- it('unlocks the radios once the controlling field is set', () => {
58
- render(
59
- <RadioField
60
- value={undefined}
61
- onChange={vi.fn()}
62
- field={provinceField}
63
- {...({ name: 'province', dependentValues: { country: 'cn' } } as any)}
64
- />,
65
- );
66
- expect(screen.queryByTestId('radio-empty-province')).not.toBeInTheDocument();
67
- // Only the `cn` option is offered.
68
- expect(screen.getByRole('radio', { name: 'Zhejiang' })).toBeInTheDocument();
69
- expect(screen.queryByRole('radio', { name: 'California' })).not.toBeInTheDocument();
70
- });
71
- });
72
-
73
- describe('RadioField — cascade clear (#2715)', () => {
74
- it('clears a value the parent change no longer offers', () => {
75
- const onChange = vi.fn();
76
- render(
77
- <RadioField
78
- value={'ca'}
79
- onChange={onChange}
80
- field={provinceField}
81
- {...({ name: 'province', dependentValues: { country: 'cn' } } as any)}
82
- />,
83
- );
84
- // 'ca' is a US province — under country=cn it is not offered, so it is dropped.
85
- expect(onChange).toHaveBeenCalledWith(undefined);
86
- });
87
-
88
- it('keeps a value that is still offered', () => {
89
- const onChange = vi.fn();
90
- render(
91
- <RadioField
92
- value={'zj'}
93
- onChange={onChange}
94
- field={provinceField}
95
- {...({ name: 'province', dependentValues: { country: 'cn' } } as any)}
96
- />,
97
- );
98
- expect(onChange).not.toHaveBeenCalled();
99
- });
100
- });
101
-
102
- describe('RadioField — role / context gating (#2715)', () => {
103
- const tierField = {
104
- name: 'tier',
105
- type: 'radio',
106
- options: [
107
- { label: 'Standard', value: 'standard' },
108
- { label: 'Admin only', value: 'admin_only', visibleWhen: "'admin' in current_user.positions" },
109
- ],
110
- } as any;
111
-
112
- it('clears an admin-only value for a non-admin (offered set excludes it)', () => {
113
- const onChange = vi.fn();
114
- render(
115
- <PredicateScopeProvider scope={{ current_user: { positions: ['sales'] } }}>
116
- <RadioField
117
- value={'admin_only'}
118
- onChange={onChange}
119
- field={tierField}
120
- {...({ name: 'tier', dependentValues: {} } as any)}
121
- />
122
- </PredicateScopeProvider>,
123
- );
124
- expect(onChange).toHaveBeenCalledWith(undefined);
125
- });
126
-
127
- it('keeps an admin-only value for an admin', () => {
128
- const onChange = vi.fn();
129
- render(
130
- <PredicateScopeProvider scope={{ current_user: { positions: ['admin'] } }}>
131
- <RadioField
132
- value={'admin_only'}
133
- onChange={onChange}
134
- field={tierField}
135
- {...({ name: 'tier', dependentValues: {} } as any)}
136
- />
137
- </PredicateScopeProvider>,
138
- );
139
- expect(onChange).not.toHaveBeenCalled();
140
- });
141
- });
@@ -1,143 +0,0 @@
1
- import React, { useId, useEffect } from 'react';
2
- import { RadioGroup, RadioGroupItem, Label, EmptyValue } from '@object-ui/components';
3
- import { isValueStillOffered, type OptionLike } from '@object-ui/core';
4
- import { FieldWidgetComponentProps } from './types';
5
- import { toDomProps } from './toDomProps';
6
- import { toHostGroupProps } from './toHostGroupProps';
7
- import { OptionsEmptyState } from './OptionsEmptyState';
8
- import { useCascadingOptions } from './useCascadingOptions';
9
-
10
- type Option = OptionLike;
11
-
12
- /**
13
- * RadioField - choose exactly one value from a fixed option set, rendered as a
14
- * radio group. The stored value is the selected option value (string). Used for
15
- * the `radio` field type.
16
- *
17
- * Like the single `SelectField`, options support per-option `visibleWhen`
18
- * cascading + `dependsOn` gating (ADR-0058 / #2715), resolved through the shared
19
- * {@link useCascadingOptions} hook: the offered radios narrow against the live
20
- * form record + `current_user`, the control is gated behind a "select the parent
21
- * first" hint while a dependency is empty, and a value no longer offered (parent
22
- * changed / predicate flipped) is cleared.
23
- */
24
- export function RadioField({
25
- value,
26
- onChange,
27
- field,
28
- readonly,
29
- className,
30
- dependentValues,
31
- dependsOn: dependsOnProp,
32
- emptyHint,
33
- dataSource: _dataSource,
34
- error,
35
- ...props
36
- }: FieldWidgetComponentProps<string>) {
37
- const config = field as any;
38
- const rawOptions: Option[] = config?.options || [];
39
- const groupId = useId();
40
- const fieldName = props.name || config?.name || props.id || '';
41
-
42
- const dependsOn = config?.dependsOn ?? dependsOnProp;
43
- const { options, gated, dependsOnFields } = useCascadingOptions<Option>(
44
- rawOptions,
45
- dependsOn,
46
- dependentValues,
47
- );
48
-
49
- // Cascade clear: once the offered set no longer includes the current value
50
- // (parent changed / predicate flipped), drop it so no stale pair persists.
51
- useEffect(() => {
52
- if (readonly) return;
53
- // Never configured → nothing to prune against; see `MultiSelectField`'s
54
- // copy of this guard for the measured failure (objectui#4220).
55
- if (rawOptions.length === 0) return;
56
- // Gated → the authored list is withheld until the `dependsOn` parent is
57
- // chosen, so the empty offered set is missing information, not a verdict on
58
- // the stored value; clearing here fired on mount (objectui#4247). Same
59
- // reasoning, at length, in `MultiSelectField`.
60
- if (gated) return;
61
- if (value === undefined || value === null || (value as unknown) === '') return;
62
- if (!isValueStillOffered(value, options)) onChange?.(undefined as unknown as string);
63
- // eslint-disable-next-line react-hooks/exhaustive-deps
64
- }, [options, gated]);
65
-
66
- // The host's group label has to be consumed on EVERY surface this widget can
67
- // render, not only the `RadioGroup` (objectui#3990): both branches below return
68
- // before that spread, which is how a field-level `readonly: true` and a
69
- // zero-option list ended up with a published label id and no consumer at all.
70
- // See `toHostGroupProps` — note the role it emits is `group`, not the
71
- // `radiogroup` the editable branch keeps: there is not one radio left in
72
- // either of these surfaces.
73
- const hostGroupProps = toHostGroupProps(props);
74
-
75
- if (readonly) {
76
- // `EmptyValue`'s own `aria-label` ("No value") is outranked by
77
- // `aria-labelledby` per accname, and on the `generic` role that placeholder
78
- // span carries it was never exposed as a name anyway.
79
- if (value == null || value === '') return <EmptyValue {...hostGroupProps} />;
80
- // Label from the raw set so a stored value hidden by `visibleWhen` still
81
- // renders its label rather than a bare id.
82
- const opt = rawOptions.find((o) => o.value === value);
83
- return <span {...hostGroupProps} className="text-sm">{opt?.label || String(value)}</span>;
84
- }
85
-
86
- // No offered options is unfillable — surface a legible state instead of an
87
- // empty radio group: the host's `emptyHint` when it computed one, else this
88
- // widget's own translated copy. Shared with the select / multiselect /
89
- // checkboxes so the four cannot drift again (objectui#3231).
90
- if (options.length === 0) {
91
- return (
92
- <OptionsEmptyState
93
- emptyHint={emptyHint}
94
- gated={gated}
95
- dependsOnFields={dependsOnFields}
96
- testId={fieldName ? `radio-empty-${fieldName}` : undefined}
97
- className="min-h-9"
98
- // This box IS the field in this state, so it is what the host label
99
- // names (objectui#3990).
100
- hostGroupProps={hostGroupProps}
101
- />
102
- );
103
- }
104
-
105
- return (
106
- // DOM pass-through onto the radiogroup (objectui#3318). It also carries the
107
- // host's group label: when the form renderer associates its `<FormLabel>` by
108
- // IDREF (`aria-labelledby`, objectui#3961) that key rides in this same
109
- // whitelist spread — `toDomProps` forwards the whole `aria-` family — so this
110
- // widget needs no branch of its own. `radiogroup` is already a member of the
111
- // group role family, so nothing here overrides it with `role="group"`: it is
112
- // the more specific role AND the correct carrier of `aria-invalid` below.
113
- //
114
- // Unlike Radix
115
- // `Select.Root` (#3306) this Root IS a real DOM element — a
116
- // `<div role="radiogroup">` — and `radiogroup` is exactly the role
117
- // WAI-ARIA designates to carry `aria-invalid` for a set of radios
118
- // (`radio` itself does not support it): the group's state is announced
119
- // when focus lands on any radio inside it.
120
- <RadioGroup
121
- {...toDomProps(props)}
122
- value={value ?? ''}
123
- onValueChange={onChange}
124
- disabled={props.disabled}
125
- className={className}
126
- // AFTER the spread so this widget's own computation wins (#3222).
127
- aria-invalid={!!error}
128
- >
129
- {options.map((opt) => {
130
- // Radix speaks strings — stringify the (possibly numeric,
131
- // #3090-widened) authored value at this boundary.
132
- const value = String(opt.value);
133
- const id = `${groupId}-${value}`;
134
- return (
135
- <div key={value} className="flex items-center space-x-2">
136
- <RadioGroupItem value={value} id={id} data-testid={`radio-option-${value}`} />
137
- <Label htmlFor={id} className="font-normal">{opt.label}</Label>
138
- </div>
139
- );
140
- })}
141
- </RadioGroup>
142
- );
143
- }