@hjmds/react-native 0.8.2

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 (94) hide show
  1. package/README.md +310 -0
  2. package/dist/actions.d.ts +128 -0
  3. package/dist/actions.d.ts.map +1 -0
  4. package/dist/actions.js +157 -0
  5. package/dist/actions.js.map +1 -0
  6. package/dist/data-display.d.ts +285 -0
  7. package/dist/data-display.d.ts.map +1 -0
  8. package/dist/data-display.js +775 -0
  9. package/dist/data-display.js.map +1 -0
  10. package/dist/date-picker.d.ts +25 -0
  11. package/dist/date-picker.d.ts.map +1 -0
  12. package/dist/date-picker.js +39 -0
  13. package/dist/date-picker.js.map +1 -0
  14. package/dist/evidence.d.ts +954 -0
  15. package/dist/evidence.d.ts.map +1 -0
  16. package/dist/evidence.js +98 -0
  17. package/dist/evidence.js.map +1 -0
  18. package/dist/feedback.d.ts +148 -0
  19. package/dist/feedback.d.ts.map +1 -0
  20. package/dist/feedback.js +559 -0
  21. package/dist/feedback.js.map +1 -0
  22. package/dist/file-picker.d.ts +20 -0
  23. package/dist/file-picker.d.ts.map +1 -0
  24. package/dist/file-picker.js +37 -0
  25. package/dist/file-picker.js.map +1 -0
  26. package/dist/forms.d.ts +170 -0
  27. package/dist/forms.d.ts.map +1 -0
  28. package/dist/forms.js +595 -0
  29. package/dist/forms.js.map +1 -0
  30. package/dist/index.d.ts +16 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +16 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/inputs-public.d.ts +6 -0
  35. package/dist/inputs-public.d.ts.map +1 -0
  36. package/dist/inputs-public.js +6 -0
  37. package/dist/inputs-public.js.map +1 -0
  38. package/dist/inputs.d.ts +258 -0
  39. package/dist/inputs.d.ts.map +1 -0
  40. package/dist/inputs.js +681 -0
  41. package/dist/inputs.js.map +1 -0
  42. package/dist/internal/modal-lifecycle.d.ts +15 -0
  43. package/dist/internal/modal-lifecycle.d.ts.map +1 -0
  44. package/dist/internal/modal-lifecycle.js +17 -0
  45. package/dist/internal/modal-lifecycle.js.map +1 -0
  46. package/dist/internal/state.d.ts +8 -0
  47. package/dist/internal/state.d.ts.map +1 -0
  48. package/dist/internal/state.js +18 -0
  49. package/dist/internal/state.js.map +1 -0
  50. package/dist/internal/styles.d.ts +29 -0
  51. package/dist/internal/styles.d.ts.map +1 -0
  52. package/dist/internal/styles.js +51 -0
  53. package/dist/internal/styles.js.map +1 -0
  54. package/dist/navigation.d.ts +271 -0
  55. package/dist/navigation.d.ts.map +1 -0
  56. package/dist/navigation.js +978 -0
  57. package/dist/navigation.js.map +1 -0
  58. package/dist/number-field.d.ts +58 -0
  59. package/dist/number-field.d.ts.map +1 -0
  60. package/dist/number-field.js +172 -0
  61. package/dist/number-field.js.map +1 -0
  62. package/dist/otp-field.d.ts +2 -0
  63. package/dist/otp-field.d.ts.map +1 -0
  64. package/dist/otp-field.js +2 -0
  65. package/dist/otp-field.js.map +1 -0
  66. package/dist/overlays.d.ts +68 -0
  67. package/dist/overlays.d.ts.map +1 -0
  68. package/dist/overlays.js +757 -0
  69. package/dist/overlays.js.map +1 -0
  70. package/dist/password-field.d.ts +2 -0
  71. package/dist/password-field.d.ts.map +1 -0
  72. package/dist/password-field.js +2 -0
  73. package/dist/password-field.js.map +1 -0
  74. package/dist/primitives.d.ts +132 -0
  75. package/dist/primitives.d.ts.map +1 -0
  76. package/dist/primitives.js +238 -0
  77. package/dist/primitives.js.map +1 -0
  78. package/dist/provider.d.ts +41 -0
  79. package/dist/provider.d.ts.map +1 -0
  80. package/dist/provider.js +78 -0
  81. package/dist/provider.js.map +1 -0
  82. package/dist/slider.d.ts +45 -0
  83. package/dist/slider.d.ts.map +1 -0
  84. package/dist/slider.js +200 -0
  85. package/dist/slider.js.map +1 -0
  86. package/dist/steps.d.ts +13 -0
  87. package/dist/steps.d.ts.map +1 -0
  88. package/dist/steps.js +13 -0
  89. package/dist/steps.js.map +1 -0
  90. package/dist/upload-item.d.ts +13 -0
  91. package/dist/upload-item.d.ts.map +1 -0
  92. package/dist/upload-item.js +25 -0
  93. package/dist/upload-item.js.map +1 -0
  94. package/package.json +166 -0
package/dist/forms.js ADDED
@@ -0,0 +1,595 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { formRecipe, } from "@hjmds/design-contracts/components/form";
3
+ import { comboboxBehaviorDefaults, resolveControlAccessibleName, } from "@hjmds/design-contracts/behaviors";
4
+ import { flattenCollectionItems, isComboboxResultCurrent, reconcileSelectSelection, resolveComboboxSelectedItem, resolveSelectSelectedItem, validateCollection, } from "@hjmds/design-contracts/components/collection";
5
+ import { resolveColorReference } from "@hjmds/design-contracts/color-references";
6
+ import { glyph, radius, spacing } from "@hjmds/design-contracts/foundations";
7
+ import { comboboxRecipe, selectRecipe, } from "@hjmds/design-contracts/recipes";
8
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
9
+ import { AccessibilityInfo, ActivityIndicator, Modal, Pressable, ScrollView, TextInput, View, findNodeHandle, } from "react-native";
10
+ import { Button } from "./actions.js";
11
+ import { useControllableState } from "./internal/state.js";
12
+ import { scheduleAfterNativeModalTeardown, shouldAwaitNativeModalDismiss, } from "./internal/modal-lifecycle.js";
13
+ import { logicalTextAlign, minimumTargetStyle, resolveNativeTextScaleProps, } from "./internal/styles.js";
14
+ import { Text } from "./primitives.js";
15
+ import { useHjmNativeTheme } from "./provider.js";
16
+ function useAfterModalDismiss(visible) {
17
+ const shownRef = useRef(false);
18
+ const previousVisibleRef = useRef(visible);
19
+ const pendingRef = useRef(null);
20
+ const teardownTaskRef = useRef(null);
21
+ const complete = useCallback(() => {
22
+ teardownTaskRef.current?.cancel();
23
+ teardownTaskRef.current = null;
24
+ shownRef.current = false;
25
+ const pending = pendingRef.current;
26
+ pendingRef.current = null;
27
+ if (pending)
28
+ void pending();
29
+ }, []);
30
+ useLayoutEffect(() => {
31
+ const wasVisible = previousVisibleRef.current;
32
+ previousVisibleRef.current = visible;
33
+ if (!wasVisible || visible || shouldAwaitNativeModalDismiss(shownRef.current))
34
+ return;
35
+ teardownTaskRef.current?.cancel();
36
+ teardownTaskRef.current = scheduleAfterNativeModalTeardown(complete);
37
+ }, [complete, visible]);
38
+ useEffect(() => () => teardownTaskRef.current?.cancel(), []);
39
+ return {
40
+ queue(action) {
41
+ pendingRef.current = action;
42
+ },
43
+ onDismiss: complete,
44
+ onShow() {
45
+ shownRef.current = true;
46
+ },
47
+ };
48
+ }
49
+ /** A renderer-neutral field frame for custom Native controls. */
50
+ export function Field({ label, children, description, error, required = false, disabled = false, style, }) {
51
+ const visibleLabel = `${label}${required ? " *" : ""}`;
52
+ const hint = error ?? description;
53
+ const controlProps = {
54
+ accessibilityLabel: visibleLabel,
55
+ ...(hint === undefined ? {} : { accessibilityHint: hint }),
56
+ accessibilityState: { disabled },
57
+ };
58
+ return (_jsxs(View, { style: [{ gap: spacing.xs }, style], children: [_jsx(Text, { tone: "primary", variant: "label", children: visibleLabel }), typeof children === "function" ? children(controlProps) : children, error ? (_jsx(Text, { accessibilityLiveRegion: "assertive", tone: "danger", variant: "caption", children: error })) : description ? (_jsx(Text, { tone: "muted", variant: "caption", children: description })) : null] }));
59
+ }
60
+ /**
61
+ * A Native submit boundary. Products retain ownership of values and validation;
62
+ * this renderer only owns submit re-entrancy, feedback, and field rhythm.
63
+ */
64
+ export function Form({ label, values, onSubmit, children, submitLabel, status, defaultStatus = "idle", onStatusChange, error, fallbackErrorMessage, disabled = false, density = "comfortable", style, }) {
65
+ const [submitStatus, setSubmitStatus] = useControllableState({
66
+ ...(status === undefined ? {} : { value: status }),
67
+ defaultValue: defaultStatus,
68
+ ...(onStatusChange === undefined ? {} : { onChange: onStatusChange }),
69
+ });
70
+ const [internalError, setInternalError] = useState();
71
+ const submittingRef = useRef(false);
72
+ const mountedRef = useRef(true);
73
+ const busy = submitStatus === "submitting";
74
+ useEffect(() => {
75
+ mountedRef.current = true;
76
+ return () => {
77
+ mountedRef.current = false;
78
+ };
79
+ }, []);
80
+ const submit = async () => {
81
+ if (disabled || busy || submittingRef.current)
82
+ return;
83
+ submittingRef.current = true;
84
+ setInternalError(undefined);
85
+ setSubmitStatus("submitting");
86
+ try {
87
+ await onSubmit(values);
88
+ if (mountedRef.current)
89
+ setSubmitStatus("succeeded");
90
+ }
91
+ catch (caught) {
92
+ if (mountedRef.current) {
93
+ setInternalError(caught instanceof Error && caught.message.trim().length > 0
94
+ ? caught.message
95
+ : fallbackErrorMessage);
96
+ setSubmitStatus("failed");
97
+ }
98
+ }
99
+ finally {
100
+ submittingRef.current = false;
101
+ }
102
+ };
103
+ return (_jsxs(View, { accessibilityLabel: label, accessibilityState: { busy, disabled }, style: [{ gap: formRecipe.density[density].fieldGap }, style], children: [children, error ?? internalError ? (_jsx(View, { accessibilityLiveRegion: "assertive", accessibilityRole: "alert", children: _jsx(Text, { tone: "danger", children: error ?? internalError }) })) : null, _jsx(Button, { disabled: disabled, loading: busy, onPress: () => void submit(), children: submitLabel })] }));
104
+ }
105
+ /** Native adaptive Select with shared sections, async states, and teardown-safe commits. */
106
+ export function Select({ label, accessibilityLabel, options, source: sourceProp, items, sections, value, defaultValue, onValueChange, selectedKey, defaultSelectedKey, onSelectionChange, selectedItem, disallowEmptySelection = false, open, defaultOpen = false, onOpenChange, placeholder, description, error, required = false, disabled = false, readOnly = false, busy = false, size = selectRecipe.defaults.size, density = selectRecipe.defaults.density, asyncState = { status: "idle" }, onRetry, retryLabel, readOnlyLabel, openHint, renderLeading, renderOptionLeading, onSelectionAfterDismiss, onDismiss, dismissLabel, optionsAccessibilityLabel, style, ...modalProps }) {
107
+ const providedSources = [sourceProp, options, items, sections].filter((candidate) => candidate !== undefined).length;
108
+ if (providedSources !== 1) {
109
+ throw new TypeError("Select requires exactly one of source, options, items, or sections");
110
+ }
111
+ if (value !== undefined && selectedKey !== undefined) {
112
+ throw new TypeError("Select cannot combine value and selectedKey");
113
+ }
114
+ if (defaultValue !== undefined && defaultSelectedKey !== undefined) {
115
+ throw new TypeError("Select cannot combine defaultValue and defaultSelectedKey");
116
+ }
117
+ const source = useMemo(() => {
118
+ if (sourceProp)
119
+ return sourceProp;
120
+ if (sections)
121
+ return { sections };
122
+ if (items)
123
+ return { items };
124
+ return {
125
+ items: (options ?? []).map((option) => ({
126
+ id: option.value,
127
+ label: option.label,
128
+ textValue: option.label,
129
+ ...(option.description === undefined ? {} : { description: option.description }),
130
+ ...(option.disabled === undefined ? {} : { disabled: option.disabled }),
131
+ })),
132
+ };
133
+ }, [items, options, sections, sourceProp]);
134
+ validateCollection(source);
135
+ const collectionItems = flattenCollectionItems(source);
136
+ if (collectionItems.length === 0 && asyncState.status === "idle") {
137
+ throw new Error("Select requires an option or a non-idle asyncState");
138
+ }
139
+ const accessibleName = resolveControlAccessibleName(label, accessibilityLabel, "Select");
140
+ const theme = useHjmNativeTheme();
141
+ const { colors, environment } = theme;
142
+ const requestedControlled = selectedKey !== undefined ? selectedKey : value;
143
+ const requestedDefault = defaultSelectedKey ?? defaultValue ?? null;
144
+ const requestedValue = requestedControlled ?? requestedDefault;
145
+ if (requestedValue !== null &&
146
+ requestedValue !== undefined &&
147
+ !collectionItems.some((option) => option.id === requestedValue) &&
148
+ selectedItem?.id !== requestedValue &&
149
+ asyncState.status === "idle") {
150
+ throw new RangeError("Select selection must match an option");
151
+ }
152
+ const [selected, setSelected] = useControllableState({
153
+ ...(requestedControlled === undefined ? {} : { value: requestedControlled }),
154
+ defaultValue: requestedDefault,
155
+ onChange: (next) => {
156
+ if (next !== null)
157
+ onValueChange?.(next);
158
+ onSelectionChange?.(next);
159
+ },
160
+ });
161
+ const reconciledSelected = reconcileSelectSelection(source, selected, {
162
+ disallowEmptySelection,
163
+ asyncState,
164
+ ...(selectedItem === undefined ? {} : { selectedItem }),
165
+ });
166
+ useEffect(() => {
167
+ if (requestedControlled === undefined && reconciledSelected !== selected) {
168
+ setSelected(reconciledSelected);
169
+ }
170
+ }, [reconciledSelected, requestedControlled, selected, setSelected]);
171
+ const resolvedSelectedItem = resolveSelectSelectedItem(source, reconciledSelected, selectedItem);
172
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
173
+ const visible = open ?? uncontrolledOpen;
174
+ const requestOpen = useCallback((next, reason) => {
175
+ if (next === visible)
176
+ return;
177
+ if (open === undefined)
178
+ setUncontrolledOpen(next);
179
+ onOpenChange?.(next, reason);
180
+ }, [onOpenChange, open, visible]);
181
+ const triggerRef = useRef(null);
182
+ const optionRefs = useRef(new Map());
183
+ const modalDismiss = useAfterModalDismiss(visible);
184
+ const sizeContract = selectRecipe.sizes[size];
185
+ const densityContract = selectRecipe.density[density];
186
+ const close = useCallback((reason, after) => {
187
+ modalDismiss.queue(async () => {
188
+ const handle = findNodeHandle(triggerRef.current);
189
+ if (handle !== null)
190
+ AccessibilityInfo.setAccessibilityFocus(handle);
191
+ onDismiss?.(reason);
192
+ await after?.();
193
+ });
194
+ requestOpen(false, reason);
195
+ }, [modalDismiss, onDismiss, requestOpen]);
196
+ useEffect(() => {
197
+ if (visible && (disabled || readOnly))
198
+ close("programmatic");
199
+ }, [close, disabled, readOnly, visible]);
200
+ const focusInitialOption = () => {
201
+ const initialValue = reconciledSelected ?? collectionItems.find((option) => !option.disabled)?.id;
202
+ if (initialValue === undefined)
203
+ return;
204
+ const target = optionRefs.current.get(initialValue);
205
+ if (target) {
206
+ const handle = findNodeHandle(target);
207
+ if (handle !== null)
208
+ AccessibilityInfo.setAccessibilityFocus(handle);
209
+ }
210
+ };
211
+ const leadingColor = resolveColorReference(selectRecipe.leading.color, theme.palette);
212
+ const triggerLeading = renderLeading?.(resolvedSelectedItem, {
213
+ placement: "trigger",
214
+ selected: resolvedSelectedItem !== null,
215
+ disabled: disabled || busy,
216
+ color: leadingColor,
217
+ size: glyph[sizeContract.glyph],
218
+ });
219
+ const renderOption = (option) => {
220
+ const checked = option.id === reconciledSelected;
221
+ const optionDisabled = option.disabled === true || disabled || readOnly || busy;
222
+ const optionLeading = renderOptionLeading?.(option, {
223
+ placement: "option",
224
+ selected: checked,
225
+ disabled: optionDisabled,
226
+ color: resolveColorReference(selectRecipe.optionLeading.color, theme.palette),
227
+ size: glyph[selectRecipe.optionLeading.glyph],
228
+ });
229
+ return (_jsxs(Pressable, { ref: (node) => {
230
+ if (node)
231
+ optionRefs.current.set(option.id, node);
232
+ else
233
+ optionRefs.current.delete(option.id);
234
+ }, accessibilityHint: option.description, accessibilityLabel: option.label, accessibilityRole: "radio", accessibilityState: { checked, disabled: optionDisabled }, disabled: optionDisabled, onPress: () => {
235
+ setSelected(option.id);
236
+ close("selection", onSelectionAfterDismiss
237
+ ? () => onSelectionAfterDismiss(option.id)
238
+ : null);
239
+ }, style: ({ pressed }) => [
240
+ minimumTargetStyle,
241
+ {
242
+ alignItems: "center",
243
+ backgroundColor: checked
244
+ ? resolveColorReference(densityContract.selectedBackground, theme.palette)
245
+ : pressed
246
+ ? resolveColorReference(densityContract.highlightedBackground, theme.palette)
247
+ : "transparent",
248
+ borderRadius: radius[densityContract.radius],
249
+ direction: environment.direction,
250
+ flexDirection: "row",
251
+ gap: densityContract.gap,
252
+ minHeight: densityContract.minHeight,
253
+ opacity: optionDisabled ? selectRecipe.states.disabledOpacity : 1,
254
+ paddingHorizontal: densityContract.paddingHorizontal,
255
+ },
256
+ ], children: [optionLeading ? (_jsx(View, { accessibilityElementsHidden: true, accessible: false, importantForAccessibility: "no-hide-descendants", children: optionLeading })) : null, _jsxs(View, { style: { flex: 1, gap: spacing.xxs, minWidth: 0 }, children: [_jsx(Text, { style: {
257
+ color: resolveColorReference(densityContract.label.color, theme.palette),
258
+ fontWeight: checked
259
+ ? selectRecipe.optionLabel.selectedFontWeight
260
+ : selectRecipe.optionLabel.fontWeight,
261
+ }, variant: densityContract.label.textVariant, children: option.label }), option.description ? (_jsx(Text, { style: { color: resolveColorReference(densityContract.description.color, theme.palette) }, variant: densityContract.description.textVariant, children: option.description })) : null] }), checked ? _jsx(Text, { accessible: false, tone: "brand", children: "\u2713" }) : null] }, option.id));
262
+ };
263
+ const collection = source.sections ? source.sections.map((section) => (_jsxs(View, { accessibilityLabel: section.accessibilityLabel ?? section.label, children: [section.label ? (_jsx(Text, { style: {
264
+ color: resolveColorReference(selectRecipe.sectionLabel.color, theme.palette),
265
+ paddingHorizontal: selectRecipe.sectionLabel.paddingHorizontal,
266
+ paddingVertical: selectRecipe.sectionLabel.paddingVertical,
267
+ }, variant: selectRecipe.sectionLabel.textVariant, children: section.label })) : null, section.items.map(renderOption)] }, section.id))) : collectionItems.map(renderOption);
268
+ const blockingState = asyncState.status === "loading" || asyncState.status === "error" || asyncState.status === "empty";
269
+ return (_jsxs(View, { style: [{ gap: spacing.xs }, style], children: [label ? _jsxs(Text, { tone: "primary", variant: "label", children: [label, required ? " *" : ""] }) : null, _jsxs(Pressable, { ref: triggerRef, accessibilityLabel: accessibleName, accessibilityHint: readOnly ? readOnlyLabel : error ?? description ?? openHint, accessibilityRole: "combobox", accessibilityState: {
270
+ busy: busy || asyncState.status === "loading",
271
+ disabled: disabled || readOnly || busy,
272
+ expanded: visible,
273
+ }, accessibilityValue: { text: resolvedSelectedItem?.label ?? placeholder }, disabled: disabled || readOnly || busy, onPress: () => {
274
+ if (!readOnly)
275
+ requestOpen(!visible, "trigger");
276
+ }, style: ({ pressed }) => [
277
+ minimumTargetStyle,
278
+ {
279
+ alignItems: "center",
280
+ backgroundColor: colors.bg,
281
+ borderColor: error ? colors.danger : colors.border,
282
+ borderRadius: radius.md,
283
+ borderWidth: error ? 2 : 1,
284
+ direction: environment.direction,
285
+ flexDirection: "row",
286
+ gap: selectRecipe.value.gap,
287
+ minHeight: sizeContract.minHeight,
288
+ opacity: disabled || busy ? selectRecipe.states.disabledOpacity : pressed ? 0.86 : 1,
289
+ paddingHorizontal: sizeContract.paddingHorizontal,
290
+ },
291
+ ], children: [triggerLeading ? (_jsx(View, { accessibilityElementsHidden: true, accessible: false, importantForAccessibility: "no-hide-descendants", children: triggerLeading })) : null, _jsx(Text, { style: { flex: 1 }, tone: resolvedSelectedItem ? "body" : "muted", variant: sizeContract.textVariant, children: resolvedSelectedItem?.label ?? placeholder }), busy ? _jsx(ActivityIndicator, { size: glyph[selectRecipe.busyIndicator.glyph] }) : _jsx(Text, { accessible: false, tone: "muted", children: "\u2304" })] }), error ? (_jsx(Text, { accessibilityLiveRegion: "assertive", tone: "danger", variant: "caption", children: error })) : description ? (_jsx(Text, { tone: "muted", variant: "caption", children: description })) : null, _jsx(Modal, { ...modalProps, animationType: "none", onDismiss: modalDismiss.onDismiss, onRequestClose: () => close("escape"), onShow: () => {
292
+ modalDismiss.onShow();
293
+ focusInitialOption();
294
+ }, transparent: true, visible: visible, children: _jsxs(View, { style: { flex: 1, justifyContent: "flex-end" }, children: [_jsx(Pressable, { accessibilityLabel: dismissLabel, accessibilityRole: "button", onPress: () => close("outside"), style: {
295
+ backgroundColor: "#00000088",
296
+ bottom: 0,
297
+ left: 0,
298
+ position: "absolute",
299
+ right: 0,
300
+ top: 0,
301
+ } }), _jsxs(View, { accessibilityLabel: optionsAccessibilityLabel ?? accessibleName, accessibilityRole: "radiogroup", accessibilityViewIsModal: true, style: {
302
+ backgroundColor: colors.bg,
303
+ borderTopLeftRadius: radius.lg,
304
+ borderTopRightRadius: radius.lg,
305
+ gap: spacing.sm,
306
+ maxHeight: "75%",
307
+ padding: spacing.md,
308
+ }, children: [_jsx(Text, { tone: "primary", variant: "title", children: label ?? accessibleName }), _jsxs(ScrollView, { children: [blockingState ? (_jsxs(View, { style: { gap: spacing.sm, minHeight: selectRecipe.stateMessage.minHeight }, children: [asyncState.status === "loading" ? _jsx(ActivityIndicator, {}) : null, _jsx(Text, { accessibilityLiveRegion: "polite", accessibilityRole: asyncState.status === "error" ? "alert" : undefined, tone: asyncState.status === "error" ? "danger" : "muted", children: asyncState.message }), asyncState.status === "error" && onRetry ? (_jsx(Button, { onPress: onRetry, tone: "secondary", children: retryLabel ?? dismissLabel })) : null] })) : collection, asyncState.status === "loadingMore" ? (_jsxs(View, { accessibilityLiveRegion: "polite", accessibilityState: { busy: true }, style: { alignItems: "center", flexDirection: "row", gap: spacing.xs }, children: [_jsx(ActivityIndicator, {}), _jsx(Text, { tone: "muted", children: asyncState.message })] })) : null] }), _jsx(Button, { onPress: () => close("programmatic"), tone: "secondary", children: dismissLabel })] })] }) })] }));
309
+ }
310
+ /** Editable Native combobox with sectioned async results and teardown-safe commits. */
311
+ export function Combobox({ label, accessibilityLabel, items, sections, source: sourceProp, selectedKey, defaultSelectedKey = null, selectedItem, onSelectionChange, inputValue, defaultInputValue, onInputValueChange, open, defaultOpen = false, onOpenChange, onCommit, onCommitAfterDismiss, onDismiss, filtering = comboboxBehaviorDefaults.filtering, queryValue, resultQuery, asyncState, loading = false, emptyMessage, loadingMessage, loadingMoreMessage, errorMessage, promptMessage, minimumQueryLength = 0, onRetry, retryLabel, description, error, placeholder, openHint, sheetTitle, required = false, disabled = false, readOnly = false, busy = false, openOnFocus = true, size = comboboxRecipe.defaults.size, density = comboboxRecipe.defaults.density, readOnlyLabel, renderLeading, clearLabel, dismissLabel, resultsAccessibilityLabel, style, ...modalProps }) {
312
+ const providedSources = [sourceProp, items, sections].filter((candidate) => candidate !== undefined).length;
313
+ if (providedSources !== 1) {
314
+ throw new TypeError("Combobox requires exactly one of source, items, or sections");
315
+ }
316
+ const source = useMemo(() => {
317
+ if (sourceProp)
318
+ return sourceProp;
319
+ if (sections)
320
+ return { sections };
321
+ return { items: items ?? [] };
322
+ }, [items, sections, sourceProp]);
323
+ validateCollection(source);
324
+ const collectionItems = flattenCollectionItems(source);
325
+ const requestedSelection = selectedKey === undefined ? defaultSelectedKey : selectedKey;
326
+ if (requestedSelection !== null && selectedItem?.id !== requestedSelection &&
327
+ !collectionItems.some((item) => item.id === requestedSelection)) {
328
+ throw new RangeError(`Combobox selectedKey needs a matching item snapshot: ${requestedSelection}`);
329
+ }
330
+ if (!emptyMessage.trim() || !loadingMessage.trim()) {
331
+ throw new TypeError("Combobox state messages must not be empty");
332
+ }
333
+ if (openHint !== undefined && !openHint.trim()) {
334
+ throw new TypeError("Combobox openHint must not be empty");
335
+ }
336
+ if (sheetTitle !== undefined && !sheetTitle.trim()) {
337
+ throw new TypeError("Combobox sheetTitle must not be empty");
338
+ }
339
+ if (!Number.isInteger(minimumQueryLength) || minimumQueryLength < 0) {
340
+ throw new RangeError("Combobox minimumQueryLength must be a non-negative integer");
341
+ }
342
+ const accessibleName = resolveControlAccessibleName(label, accessibilityLabel, "Combobox");
343
+ const theme = useHjmNativeTheme();
344
+ const { colors, environment } = theme;
345
+ const [committedKey, setCommittedKey] = useControllableState({
346
+ ...(selectedKey === undefined ? {} : { value: selectedKey }),
347
+ defaultValue: defaultSelectedKey,
348
+ ...(onSelectionChange === undefined ? {} : { onChange: onSelectionChange }),
349
+ });
350
+ const committedItem = resolveComboboxSelectedItem(source, committedKey, selectedItem);
351
+ const [query, setQuery] = useControllableState({
352
+ ...(inputValue === undefined ? {} : { value: inputValue }),
353
+ defaultValue: defaultInputValue ?? committedItem?.label ?? "",
354
+ ...(onInputValueChange === undefined ? {} : { onChange: onInputValueChange }),
355
+ });
356
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
357
+ const visible = open ?? uncontrolledOpen;
358
+ const requestOpen = useCallback((next, reason) => {
359
+ if (next === visible)
360
+ return;
361
+ if (open === undefined)
362
+ setUncontrolledOpen(next);
363
+ onOpenChange?.(next, reason);
364
+ }, [onOpenChange, open, visible]);
365
+ const [activeIndex, setActiveIndex] = useState(-1);
366
+ const inputRef = useRef(null);
367
+ const optionRefs = useRef(new Map());
368
+ const modalDismiss = useAfterModalDismiss(visible);
369
+ const normalizedQuery = query.trim().toLocaleLowerCase();
370
+ const resultsAreCurrent = filtering !== "external" ||
371
+ isComboboxResultCurrent(queryValue ?? query, resultQuery ?? query);
372
+ const filteredItems = useMemo(() => filtering === "external"
373
+ ? collectionItems
374
+ : collectionItems.filter((item) => `${item.label} ${item.textValue}`.toLocaleLowerCase().includes(normalizedQuery)), [collectionItems, filtering, normalizedQuery]);
375
+ const resolvedAsyncState = asyncState ??
376
+ (loading ? { status: "loading", message: loadingMessage } : { status: "idle" });
377
+ const sizeContract = comboboxRecipe.sizes[size];
378
+ const densityContract = comboboxRecipe.density[density];
379
+ const inputTypography = theme.tokens.typography[sizeContract.textVariant];
380
+ const inputTextScaleProps = resolveNativeTextScaleProps(theme.textScaling, {
381
+ color: colors.text,
382
+ flex: 1,
383
+ fontSize: inputTypography.fontSize,
384
+ fontWeight: inputTypography.fontWeight,
385
+ lineHeight: inputTypography.lineHeight,
386
+ minHeight: sizeContract.minHeight,
387
+ textAlign: logicalTextAlign(environment.direction),
388
+ });
389
+ const close = useCallback((reason, after) => {
390
+ modalDismiss.queue(async () => {
391
+ const handle = findNodeHandle(inputRef.current);
392
+ if (handle !== null)
393
+ AccessibilityInfo.setAccessibilityFocus(handle);
394
+ onDismiss?.(reason);
395
+ await after?.();
396
+ });
397
+ requestOpen(false, reason);
398
+ setActiveIndex(-1);
399
+ }, [modalDismiss, onDismiss, requestOpen]);
400
+ useEffect(() => {
401
+ if (visible && (disabled || readOnly))
402
+ close("programmatic");
403
+ }, [close, disabled, readOnly, visible]);
404
+ const firstEnabledIndex = () => filteredItems.findIndex((item) => !item.disabled);
405
+ const lastEnabledIndex = () => {
406
+ for (let index = filteredItems.length - 1; index >= 0; index -= 1) {
407
+ if (!filteredItems[index]?.disabled)
408
+ return index;
409
+ }
410
+ return -1;
411
+ };
412
+ const moveActive = (delta) => {
413
+ if (filteredItems.length === 0)
414
+ return;
415
+ let next = activeIndex;
416
+ for (let offset = 0; offset < filteredItems.length; offset += 1) {
417
+ next = (next + delta + filteredItems.length) % filteredItems.length;
418
+ if (!filteredItems[next]?.disabled) {
419
+ setActiveIndex(next);
420
+ return;
421
+ }
422
+ }
423
+ };
424
+ const restoreCommittedQuery = () => setQuery(committedItem?.label ?? "");
425
+ const dismiss = (reason) => {
426
+ restoreCommittedQuery();
427
+ close(reason);
428
+ };
429
+ const commit = (item) => {
430
+ if (item.disabled || disabled || readOnly || busy || !resultsAreCurrent)
431
+ return;
432
+ setCommittedKey(item.id);
433
+ setQuery(item.label);
434
+ onCommit?.(item.id, "selection");
435
+ close("selection", onCommitAfterDismiss
436
+ ? () => onCommitAfterDismiss(item.id, "selection")
437
+ : null);
438
+ };
439
+ const clear = () => {
440
+ setCommittedKey(null);
441
+ setQuery("");
442
+ onCommit?.(null, "clear");
443
+ };
444
+ const focusInitialOption = () => {
445
+ const initial = (committedKey === null ? undefined : optionRefs.current.get(committedKey)) ??
446
+ optionRefs.current.get(filteredItems.find((item) => !item.disabled)?.id);
447
+ if (initial) {
448
+ const handle = findNodeHandle(initial);
449
+ if (handle !== null)
450
+ AccessibilityInfo.setAccessibilityFocus(handle);
451
+ }
452
+ };
453
+ const leading = committedItem && renderLeading ? renderLeading(committedItem, {
454
+ placement: "trigger",
455
+ selected: true,
456
+ disabled: disabled || busy,
457
+ color: resolveColorReference(comboboxRecipe.leading.color, theme.palette),
458
+ size: glyph[sizeContract.glyph],
459
+ }) : null;
460
+ const queryTooShort = query.trim().length < minimumQueryLength;
461
+ const viewStatus = queryTooShort
462
+ ? "prompt"
463
+ : !resultsAreCurrent || resolvedAsyncState.status === "loading"
464
+ ? "loading"
465
+ : resolvedAsyncState.status === "error"
466
+ ? "error"
467
+ : resolvedAsyncState.status === "empty" || filteredItems.length === 0
468
+ ? "empty"
469
+ : resolvedAsyncState.status === "loadingMore"
470
+ ? "loadingMore"
471
+ : "ready";
472
+ const stateMessage = viewStatus === "prompt"
473
+ ? promptMessage ?? emptyMessage
474
+ : viewStatus === "loading"
475
+ ? resolvedAsyncState.status === "loading" ? resolvedAsyncState.message : loadingMessage
476
+ : viewStatus === "error"
477
+ ? resolvedAsyncState.status === "error" ? resolvedAsyncState.message : errorMessage ?? emptyMessage
478
+ : viewStatus === "empty"
479
+ ? resolvedAsyncState.status === "empty" ? resolvedAsyncState.message : emptyMessage
480
+ : viewStatus === "loadingMore"
481
+ ? resolvedAsyncState.status === "loadingMore" ? resolvedAsyncState.message : loadingMoreMessage ?? loadingMessage
482
+ : "";
483
+ const filteredIds = new Set(filteredItems.map((item) => item.id));
484
+ const renderOption = (item, index) => {
485
+ const checked = item.id === committedKey;
486
+ const active = index === activeIndex;
487
+ const itemDisabled = item.disabled === true || disabled || readOnly || busy || !resultsAreCurrent;
488
+ const optionLeading = renderLeading?.(item, {
489
+ placement: "option",
490
+ selected: checked,
491
+ disabled: itemDisabled,
492
+ color: resolveColorReference(comboboxRecipe.optionLeading.color, theme.palette),
493
+ size: glyph[comboboxRecipe.optionLeading.glyph],
494
+ });
495
+ return (_jsxs(Pressable, { ref: (node) => {
496
+ if (node)
497
+ optionRefs.current.set(item.id, node);
498
+ else
499
+ optionRefs.current.delete(item.id);
500
+ }, accessibilityHint: item.description, accessibilityLabel: item.label, accessibilityRole: "radio", accessibilityState: { checked, disabled: itemDisabled }, disabled: itemDisabled, onPress: () => commit(item), style: ({ pressed }) => [
501
+ minimumTargetStyle,
502
+ {
503
+ alignItems: "center",
504
+ backgroundColor: checked || active
505
+ ? resolveColorReference(densityContract.selectedBackground, theme.palette)
506
+ : pressed
507
+ ? resolveColorReference(comboboxRecipe.states.pressedBackground, theme.palette)
508
+ : "transparent",
509
+ borderRadius: radius[densityContract.radius],
510
+ direction: environment.direction,
511
+ flexDirection: "row",
512
+ gap: densityContract.gap,
513
+ minHeight: densityContract.minHeight,
514
+ opacity: itemDisabled ? comboboxRecipe.states.disabledOpacity : 1,
515
+ paddingHorizontal: densityContract.paddingHorizontal,
516
+ },
517
+ ], children: [optionLeading ? (_jsx(View, { accessibilityElementsHidden: true, accessible: false, importantForAccessibility: "no-hide-descendants", children: optionLeading })) : null, _jsxs(View, { style: { flex: 1, gap: spacing.xxs, minWidth: 0 }, children: [_jsx(Text, { tone: checked ? "brand" : "body", variant: densityContract.label.textVariant, children: item.label }), item.description ? _jsx(Text, { tone: "muted", variant: densityContract.description.textVariant, children: item.description }) : null] }), checked ? _jsx(Text, { accessible: false, tone: "brand", children: "\u2713" }) : null] }, item.id));
518
+ };
519
+ let optionIndex = -1;
520
+ const collection = source.sections ? source.sections.map((section) => {
521
+ const visibleItems = section.items.filter((item) => filteredIds.has(item.id));
522
+ if (visibleItems.length === 0)
523
+ return null;
524
+ return (_jsxs(View, { accessibilityLabel: section.accessibilityLabel ?? section.label, children: [section.label ? (_jsx(Text, { style: {
525
+ color: resolveColorReference(comboboxRecipe.sectionLabel.color, theme.palette),
526
+ paddingHorizontal: comboboxRecipe.sectionLabel.paddingHorizontal,
527
+ paddingVertical: comboboxRecipe.sectionLabel.paddingVertical,
528
+ }, variant: comboboxRecipe.sectionLabel.textVariant, children: section.label })) : null, visibleItems.map((item) => {
529
+ optionIndex += 1;
530
+ return renderOption(item, optionIndex);
531
+ })] }, section.id));
532
+ }) : filteredItems.map(renderOption);
533
+ return (_jsxs(View, { style: [{ gap: spacing.xs }, style], children: [label ? _jsxs(Text, { tone: "primary", variant: "label", children: [label, required ? " *" : ""] }) : null, _jsxs(View, { style: {
534
+ alignItems: "center",
535
+ backgroundColor: colors.bg,
536
+ borderColor: error ? colors.danger : colors.border,
537
+ borderRadius: radius.md,
538
+ borderWidth: error ? 2 : 1,
539
+ direction: environment.direction,
540
+ flexDirection: "row",
541
+ minHeight: sizeContract.minHeight,
542
+ paddingStart: spacing.sm,
543
+ }, children: [leading ? (_jsx(View, { accessibilityElementsHidden: true, accessible: false, importantForAccessibility: "no-hide-descendants", children: leading })) : null, _jsx(TextInput, { ...inputTextScaleProps, ref: inputRef, accessibilityHint: readOnly
544
+ ? readOnlyLabel
545
+ : [error ?? description, openHint].filter(Boolean).join(". ") || undefined, accessibilityLabel: accessibleName, accessibilityRole: "combobox", accessibilityState: {
546
+ busy: busy || viewStatus === "loading" || viewStatus === "loadingMore",
547
+ disabled: disabled || readOnly || busy,
548
+ expanded: visible,
549
+ }, editable: !disabled && !readOnly && !busy, onChangeText: (next) => {
550
+ setQuery(next);
551
+ setActiveIndex(-1);
552
+ if (!visible && !readOnly)
553
+ requestOpen(true, "keyboard");
554
+ }, onFocus: () => {
555
+ if (openOnFocus && !disabled && !readOnly && !busy)
556
+ requestOpen(true, "trigger");
557
+ }, onKeyPress: (event) => {
558
+ const key = event.nativeEvent.key;
559
+ if (key === "Escape") {
560
+ dismiss("escape");
561
+ }
562
+ else if (key === "ArrowDown") {
563
+ if (!visible)
564
+ requestOpen(true, "keyboard");
565
+ moveActive(1);
566
+ }
567
+ else if (key === "ArrowUp") {
568
+ if (!visible)
569
+ requestOpen(true, "keyboard");
570
+ moveActive(-1);
571
+ }
572
+ else if (key === "Home") {
573
+ setActiveIndex(firstEnabledIndex());
574
+ }
575
+ else if (key === "End") {
576
+ setActiveIndex(lastEnabledIndex());
577
+ }
578
+ else if (key === "Enter" && activeIndex >= 0) {
579
+ const active = filteredItems[activeIndex];
580
+ if (active)
581
+ commit(active);
582
+ }
583
+ }, placeholder: placeholder, placeholderTextColor: colors.textWeak, value: query }), query.length > 0 && !readOnly ? (_jsx(Pressable, { accessibilityLabel: clearLabel, accessibilityRole: "button", disabled: disabled || busy || viewStatus === "loading", onPress: clear, style: minimumTargetStyle, children: _jsx(Text, { align: "center", tone: "muted", variant: "title", children: "\u00D7" }) })) : null] }), error ? (_jsx(Text, { accessibilityLiveRegion: "assertive", tone: "danger", variant: "caption", children: error })) : description ? (_jsx(Text, { tone: "muted", variant: "caption", children: description })) : null, _jsx(Modal, { ...modalProps, animationType: "none", onDismiss: modalDismiss.onDismiss, onRequestClose: () => dismiss("escape"), onShow: () => {
584
+ modalDismiss.onShow();
585
+ focusInitialOption();
586
+ }, transparent: true, visible: visible, children: _jsxs(View, { style: { flex: 1, justifyContent: "flex-end" }, children: [_jsx(Pressable, { accessibilityLabel: dismissLabel, accessibilityRole: "button", onPress: () => dismiss("outside"), style: { backgroundColor: "#00000088", bottom: 0, left: 0, position: "absolute", right: 0, top: 0 } }), _jsxs(View, { accessibilityLabel: resultsAccessibilityLabel ?? accessibleName, accessibilityRole: "radiogroup", accessibilityViewIsModal: true, style: {
587
+ backgroundColor: colors.bg,
588
+ borderTopLeftRadius: radius.lg,
589
+ borderTopRightRadius: radius.lg,
590
+ gap: spacing.sm,
591
+ maxHeight: "75%",
592
+ padding: spacing.md,
593
+ }, children: [_jsx(Text, { tone: "primary", variant: "title", children: sheetTitle ?? label ?? accessibleName }), viewStatus === "loading" || viewStatus === "prompt" || viewStatus === "error" || viewStatus === "empty" ? (_jsxs(View, { style: { gap: spacing.sm, minHeight: comboboxRecipe.stateMessage.minHeight }, children: [viewStatus === "loading" ? _jsx(ActivityIndicator, {}) : null, _jsx(Text, { accessibilityLiveRegion: viewStatus === "error" ? "assertive" : "polite", accessibilityRole: viewStatus === "error" ? "alert" : undefined, tone: viewStatus === "error" ? "danger" : "muted", children: stateMessage }), viewStatus === "error" && onRetry ? (_jsx(Button, { onPress: onRetry, tone: "secondary", children: retryLabel ?? dismissLabel })) : null] })) : (_jsxs(ScrollView, { keyboardShouldPersistTaps: "handled", children: [collection, viewStatus === "loadingMore" ? (_jsxs(View, { accessibilityLiveRegion: "polite", accessibilityState: { busy: true }, style: { alignItems: "center", flexDirection: "row", gap: spacing.xs }, children: [_jsx(ActivityIndicator, {}), _jsx(Text, { tone: "muted", children: stateMessage || loadingMoreMessage || loadingMessage })] })) : null] })), _jsx(Button, { onPress: () => dismiss("programmatic"), tone: "secondary", children: dismissLabel })] })] }) })] }));
594
+ }
595
+ //# sourceMappingURL=forms.js.map